Bug 1467840 - Migrate GeckoService to JobIntentService; draft
authorPetru Lingurar <petru.lingurar@softvision.ro>
Tue, 26 Jun 2018 18:41:38 +0300
changeset 818690 d5424b78fcc5e33da03574dd36523ea53376a2d5
parent 817979 b6dd3347bba6f86e934ca0b256b3629f7eed39ef
child 818691 c2f0985a71289896879a696ef0b52c957cd29cdb
push id116321
push userplingurar@mozilla.com
push dateMon, 16 Jul 2018 12:31:53 +0000
bugs1467840
milestone63.0a1
Bug 1467840 - Migrate GeckoService to JobIntentService; This simple Service needed to be migrated to JobIntentService because it could be started from background and we don't want it as a foreground service (with a notification). (For example: when the app is updated org.mozilla.gecko.PackageReplacedReceiver would try and start this service. If in background, the app would crash) Had to break the initial Service into separate JobIntentServices because in the event that there are concurrent calls (even with different Intent actions) JobScheduler would assume they are for the same already running service. INTENT_ACTION_UPDATE_ADDONS was removed as it was being unused. MozReview-Commit-ID: 2GiWFZdAVvp
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/java/org/mozilla/gecko/GeckoLoadLibsService.java
mobile/android/base/java/org/mozilla/gecko/GeckoService.java
mobile/android/base/java/org/mozilla/gecko/GeckoServicesCreatorService.java
mobile/android/base/java/org/mozilla/gecko/GeckoStarterService.java
mobile/android/base/java/org/mozilla/gecko/JobIdsConstants.java
mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java
mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java
mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java
mobile/android/base/java/org/mozilla/gecko/push/PushService.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceRegistrator.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -179,17 +179,33 @@
 
             <!-- For debugging -->
             <intent-filter>
                 <action android:name="org.mozilla.gecko.DEBUG" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity-alias>
 
-        <service android:name="org.mozilla.gecko.GeckoService" />
+        <service
+            android:name="org.mozilla.gecko.GeckoStarterService"
+            android:exported="false"
+            android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
+
+        <service
+            android:name="org.mozilla.gecko.GeckoLoadLibsService"
+            android:exported="false"
+            android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
+
+        <service
+            android:name="org.mozilla.gecko.GeckoServicesCreatorService"
+            android:exported="false"
+            android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
 
         <activity android:name="org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt"
                   android:launchMode="singleTop"
                   android:theme="@style/OverlayActivity" />
 
         <activity android:name="org.mozilla.gecko.promotion.SimpleHelperUI"
                   android:launchMode="singleTop"
                   android:theme="@style/OverlayActivity" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoLoadLibsService.java
@@ -0,0 +1,22 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+
+public class GeckoLoadLibsService extends GeckoService {
+    @Override
+    protected void onHandleWork(@NonNull Intent intent) {
+        // Intentionally not initialize Gecko when only loading libs.
+        // All work this service does is in onCreate()
+    }
+
+    public static void enqueueWork(@NonNull final Context context, @NonNull final Intent workIntent) {
+        enqueueWork(context, GeckoLoadLibsService.class, JobIdsConstants.getIdForGeckoLibsLoader(), workIntent);
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
@@ -1,77 +1,74 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import android.app.AlarmManager;
-import android.app.Service;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
-import android.os.IBinder;
-import android.os.Handler;
-import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.v4.app.JobIntentService;
 import android.util.Log;
 
+import org.mozilla.gecko.mozglue.SafeIntent;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
+
 import java.io.File;
 
-import org.mozilla.gecko.mozglue.SafeIntent;
-import org.mozilla.gecko.util.BundleEventListener;
-import org.mozilla.gecko.util.GeckoBundle;
-import org.mozilla.gecko.util.EventCallback;
-
-public class GeckoService extends Service {
+public abstract class GeckoService extends JobIntentService {
 
     private static final String LOGTAG = "GeckoService";
     private static final boolean DEBUG = false;
 
     private static final String INTENT_PROFILE_NAME = "org.mozilla.gecko.intent.PROFILE_NAME";
     private static final String INTENT_PROFILE_DIR = "org.mozilla.gecko.intent.PROFILE_DIR";
 
-    private static final String INTENT_ACTION_UPDATE_ADDONS = "update-addons";
-    private static final String INTENT_ACTION_CREATE_SERVICES = "create-services";
-    private static final String INTENT_ACTION_LOAD_LIBS = "load-libs";
-    private static final String INTENT_ACTION_START_GECKO = "start-gecko";
+    protected static final String INTENT_ACTION_CREATE_SERVICES = "create-services";
+    protected static final String INTENT_ACTION_LOAD_LIBS = "load-libs";
+    protected static final String INTENT_ACTION_START_GECKO = "start-gecko";
 
-    private static final String INTENT_SERVICE_CATEGORY = "category";
-    private static final String INTENT_SERVICE_DATA = "data";
+    protected static final String INTENT_SERVICE_CATEGORY = "category";
+    protected static final String INTENT_SERVICE_DATA = "data";
 
     private static class EventListener implements BundleEventListener {
         @Override // BundleEventListener
         public void handleMessage(final String event,
                                   final GeckoBundle message,
                                   final EventCallback callback) {
             final Context context = GeckoAppShell.getApplicationContext();
             switch (event) {
-            case "Gecko:ScheduleRun":
-                if (DEBUG) {
-                    Log.d(LOGTAG, "Scheduling " + message.getString("action") +
-                                  " @ " + message.getInt("interval") + "ms");
-                }
+                case "Gecko:ScheduleRun":
+                    if (DEBUG) {
+                        Log.d(LOGTAG, "Scheduling " + message.getString("action") +
+                                " @ " + message.getInt("interval") + "ms");
+                    }
 
-                final Intent intent = getIntentForAction(context, message.getString("action"));
-                final PendingIntent pendingIntent = PendingIntent.getService(
-                        context, /* requestCode */ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+                    final Intent intent = getIntentForAction(message.getString("action"));
+                    final PendingIntent pendingIntent = PendingIntent.getService(
+                            context, /* requestCode */ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
 
-                final AlarmManager am = (AlarmManager)
-                    context.getSystemService(Context.ALARM_SERVICE);
-                // Cancel any previous alarm and schedule a new one.
-                am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
-                                       message.getInt("trigger"),
-                                       message.getInt("interval"),
-                                       pendingIntent);
-                break;
+                    final AlarmManager am = (AlarmManager)
+                            context.getSystemService(Context.ALARM_SERVICE);
+                    // Cancel any previous alarm and schedule a new one.
+                    am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
+                            message.getInt("trigger"),
+                            message.getInt("interval"),
+                            pendingIntent);
+                    break;
 
-            default:
-                throw new UnsupportedOperationException(event);
+                default:
+                    throw new UnsupportedOperationException(event);
             }
         }
     }
 
     private static final EventListener EVENT_LISTENER = new EventListener();
 
     public static void register() {
         if (DEBUG) {
@@ -110,56 +107,84 @@ public class GeckoService extends Servic
         }
 
         if (DEBUG) {
             Log.d(LOGTAG, "Destroyed");
         }
         super.onDestroy();
     }
 
-    private static Intent getIntentForAction(final Context context, final String action) {
-        final Intent intent = new Intent(action, /* uri */ null, context, GeckoService.class);
+    @Override
+    protected abstract void onHandleWork(@NonNull Intent intent);
+
+    @Override
+    public boolean onStopCurrentWork() {
+        return false;   // do not restart if system stopped us before completing our work
+    }
+
+    private static Intent getIntentForAction(final String action) {
+        final Intent intent = new Intent(action);
         final Bundle extras = GeckoThread.getActiveExtras();
         if (extras != null && extras.size() > 0) {
             intent.replaceExtras(extras);
         }
 
         final GeckoProfile profile = GeckoThread.getActiveProfile();
         if (profile != null) {
             setIntentProfile(intent, profile.getName(), profile.getDir().getAbsolutePath());
         }
         return intent;
     }
 
-    public static Intent getIntentToCreateServices(final Context context, final String category, final String data) {
-        final Intent intent = getIntentForAction(context, INTENT_ACTION_CREATE_SERVICES);
+    public static Intent getIntentToCreateServices(final String category, final String data) {
+        final Intent intent = getIntentForAction(INTENT_ACTION_CREATE_SERVICES);
         intent.putExtra(INTENT_SERVICE_CATEGORY, category);
         intent.putExtra(INTENT_SERVICE_DATA, data);
         return intent;
     }
 
-    public static Intent getIntentToCreateServices(final Context context, final String category) {
-        return getIntentToCreateServices(context, category, /* data */ null);
+    public static Intent getIntentToCreateServices(final String category) {
+        return getIntentToCreateServices(category, /* data */ null);
     }
 
-    public static Intent getIntentToLoadLibs(final Context context) {
-        return getIntentForAction(context, INTENT_ACTION_LOAD_LIBS);
+    public static Intent getIntentToLoadLibs() {
+        return getIntentForAction(INTENT_ACTION_LOAD_LIBS);
     }
 
-    public static Intent getIntentToStartGecko(final Context context) {
-        return getIntentForAction(context, INTENT_ACTION_START_GECKO);
+    public static Intent getIntentToStartGecko() {
+        return getIntentForAction(INTENT_ACTION_START_GECKO);
     }
 
     public static void setIntentProfile(final Intent intent, final String profileName,
                                         final String profileDir) {
         intent.putExtra(INTENT_PROFILE_NAME, profileName);
         intent.putExtra(INTENT_PROFILE_DIR, profileDir);
     }
 
-    private boolean initGecko(final Intent intent) {
+    protected boolean isStartingIntentValid(@NonNull final Intent intent, @NonNull final String expectedAction) {
+        final String action = intent.getAction();
+
+        if (action == null) {
+            Log.w(LOGTAG, "Unknown request. No action to act on.");
+            return false;
+        }
+
+        if (!action.equals(expectedAction)) {
+            Log.w(LOGTAG, String.format("Unknown request: \"%s\"", action));
+            return false;
+        }
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "Handling " + intent.getAction());
+        }
+
+        return true;
+    }
+
+    protected boolean initGecko(final Intent intent) {
         if (INTENT_ACTION_LOAD_LIBS.equals(intent.getAction())) {
             // Intentionally not initialize Gecko when only loading libs.
             return true;
         }
 
         final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME);
         final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR);
 
@@ -180,65 +205,9 @@ public class GeckoService extends Servic
         } else {
             args = "-P " + profileName;
         }
 
         intent.putExtra(GeckoThread.EXTRA_ARGS, args);
         GeckoApplication.createRuntime(this, new SafeIntent(intent));
         return true;
     }
-
-    private int handleIntent(final Intent intent, final int startId) {
-        if (DEBUG) {
-            Log.d(LOGTAG, "Handling " + intent.getAction());
-        }
-
-        if (!initGecko(intent)) {
-            stopSelf(startId);
-            return Service.START_NOT_STICKY;
-        }
-
-        switch (intent.getAction()) {
-        case INTENT_ACTION_UPDATE_ADDONS:
-            // Run the add-on update service. Because the service is automatically invoked
-            // when loading Gecko, we don't have to do anything else here.
-        case INTENT_ACTION_LOAD_LIBS:
-            // Load libs only. Don't take any additional actions.
-        case INTENT_ACTION_START_GECKO:
-            // Load libs and start Gecko. Don't take any additional actions.
-            break;
-
-        case INTENT_ACTION_CREATE_SERVICES:
-            final String category = intent.getStringExtra(INTENT_SERVICE_CATEGORY);
-            final String data = intent.getStringExtra(INTENT_SERVICE_DATA);
-
-            if (category == null) {
-                break;
-            }
-            GeckoThread.createServices(category, data);
-            break;
-
-        default:
-            Log.w(LOGTAG, "Unknown request: " + intent);
-        }
-
-        stopSelf(startId);
-        return Service.START_NOT_STICKY;
-    }
-
-    @Override // Service
-    public int onStartCommand(final Intent intent, final int flags, final int startId) {
-        if (intent == null) {
-            return Service.START_NOT_STICKY;
-        }
-        try {
-            return handleIntent(intent, startId);
-        } catch (final Throwable e) {
-            Log.e(LOGTAG, "Cannot handle intent: " + intent, e);
-            return Service.START_NOT_STICKY;
-        }
-    }
-
-    @Override // Service
-    public IBinder onBind(final Intent intent) {
-        return null;
-    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoServicesCreatorService.java
@@ -0,0 +1,38 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+
+public class GeckoServicesCreatorService extends GeckoService {
+    private static final String LOGTAG = "GeckoSrvCreatorService";
+
+    @Override
+    protected void onHandleWork(@NonNull Intent intent) {
+        if (!isStartingIntentValid(intent, INTENT_ACTION_CREATE_SERVICES)) {
+            return;
+        }
+
+        if (!initGecko(intent)) {
+            return;
+        }
+
+        final String category = intent.getStringExtra(INTENT_SERVICE_CATEGORY);
+        final String data = intent.getStringExtra(INTENT_SERVICE_DATA);
+
+        if (category == null) {
+            return;
+        }
+
+        GeckoThread.createServices(category, data);
+    }
+
+    public static void enqueueWork(@NonNull final Context context, @NonNull final Intent workIntent) {
+        enqueueWork(context, GeckoServicesCreatorService.class, JobIdsConstants.getIdForGeckoServicesCreator(), workIntent);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoStarterService.java
@@ -0,0 +1,30 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+
+/**
+ * Service used to try and initialize Gecko
+ */
+public class GeckoStarterService extends GeckoService {
+    private static final String LOGTAG = "GeckoStarterService";
+
+    @Override
+    protected void onHandleWork(@NonNull Intent intent) {
+        if (!isStartingIntentValid(intent, INTENT_ACTION_START_GECKO)) {
+            return;
+        }
+
+        initGecko(intent);
+    }
+
+    public static void enqueueWork(@NonNull final Context context, @NonNull final Intent workIntent) {
+        enqueueWork(context, GeckoStarterService.class, JobIdsConstants.getIdForGeckoStarter(), workIntent);
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/JobIdsConstants.java
+++ b/mobile/android/base/java/org/mozilla/gecko/JobIdsConstants.java
@@ -38,16 +38,20 @@ public class JobIdsConstants {
 
     private static final int JOB_ID_UPDATES_REGISTER = 1009;
     private static final int JOB_ID_UPDATES_CHECK_FOR = 1010;
     private static final int JOB_ID_UPDATES_DOWNLOAD = 1011;
     private static final int JOB_ID_UPDATES_APPLY = 1012;
 
     private static final int JOB_ID_CRASH_REPORTER = 1013;
 
+    private static final int JOB_ID_GECKO_STARTER = 1014;
+    private static final int JOB_ID_GECKO_LIBS_LOADER = 1015;
+    private static final int JOB_ID_GECKO_SERVICES_CREATOR = 1016;
+
     public static int getIdForDlcStudyJob() {
         return getIdWithOffset(JOB_ID_DLC_STUDY);
     }
 
     public static int getIdForDlcDownloadJob() {
         return getIdWithOffset(JOB_ID_DLC_DOWNLOAD);
     }
 
@@ -94,16 +98,28 @@ public class JobIdsConstants {
     public static int getIdForUpdatesApplyJob() {
         return getIdWithOffset(JOB_ID_UPDATES_APPLY);
     }
 
     public static int getIdForCrashReporter() {
         return getIdWithOffset(JOB_ID_CRASH_REPORTER);
     }
 
+    public static int getIdForGeckoStarter() {
+        return getIdWithOffset(JOB_ID_GECKO_STARTER);
+    }
+
+    public static int getIdForGeckoLibsLoader() {
+        return getIdWithOffset(JOB_ID_GECKO_LIBS_LOADER);
+    }
+
+    public static int getIdForGeckoServicesCreator() {
+        return getIdWithOffset(JOB_ID_GECKO_SERVICES_CREATOR);
+    }
+
     private static boolean isReleaseBuild() {
         return AppConstants.RELEASE_OR_BETA;
     }
 
     private static int getIdWithOffset(final int jobIdUsedInRelease) {
         return isReleaseBuild() ? jobIdUsedInRelease : jobIdUsedInRelease + BETA_OFFSET;
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java
+++ b/mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java
@@ -3,29 +3,28 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-
-import org.mozilla.gecko.GeckoService;
+import android.util.Log;
 
 /**
  * This broadcast receiver receives ACTION_MY_PACKAGE_REPLACED broadcasts and
  * starts procedures that should run after the APK has been updated.
  */
 public class PackageReplacedReceiver extends BroadcastReceiver {
     public static final String ACTION_MY_PACKAGE_REPLACED = "android.intent.action.MY_PACKAGE_REPLACED";
 
     @Override
     public void onReceive(Context context, Intent intent) {
         if (intent == null || !ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) {
             // This is not the broadcast we are looking for.
             return;
         }
 
         // Load new Gecko libs to extract them to cache.
-        context.startService(GeckoService.getIntentToLoadLibs(context));
+        GeckoLoadLibsService.enqueueWork(context, GeckoService.getIntentToLoadLibs());
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java
@@ -9,16 +9,17 @@ import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.customtabs.CustomTabsService;
 import android.support.customtabs.CustomTabsSessionToken;
 import android.util.Log;
 
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoService;
+import org.mozilla.gecko.GeckoStarterService;
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 
 import java.util.List;
 
 /**
  * Custom tabs service external, third-party apps connect to.
@@ -43,21 +44,21 @@ public class GeckoCustomTabsService exte
         if (DEBUG) {
             Log.v(LOGTAG, "warming up...");
         }
 
         if (GeckoThread.isRunning()) {
             return true;
         }
 
-        final Intent intent = GeckoService.getIntentToStartGecko(this);
+        final Intent intent = GeckoService.getIntentToStartGecko();
         // Use a default profile for warming up Gecko.
         final GeckoProfile profile = GeckoProfile.get(this);
         GeckoService.setIntentProfile(intent, profile.getName(), profile.getDir().getAbsolutePath());
-        startService(intent);
+        GeckoStarterService.enqueueWork(this, intent);
         return true;
     }
 
     @Override
     protected boolean newSession(CustomTabsSessionToken sessionToken) {
         Log.v(LOGTAG, "newSession()");
 
         // Pretend session has been started
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
@@ -3,36 +3,33 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.notifications;
 
 import android.app.Activity;
 import android.app.Notification;
 import android.app.PendingIntent;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.support.v4.app.NotificationCompat;
 import android.support.v4.app.NotificationManagerCompat;
-import android.util.Log;
-
-import java.util.HashMap;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoActivityMonitor;
-import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoService;
 import org.mozilla.gecko.NotificationListener;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.util.BitmapUtils;
 
+import java.util.HashMap;
+
 /**
  * Client for posting notifications.
  */
 public final class NotificationClient implements NotificationListener {
     private static final String LOGTAG = "GeckoNotificationClient";
     /* package */ static final String CLICK_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".NOTIFICATION_CLICK";
     /* package */ static final String CLOSE_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".NOTIFICATION_CLOSE";
     /* package */ static final String PERSISTENT_INTENT_EXTRA = "persistentIntent";
@@ -94,30 +91,30 @@ public final class NotificationClient im
                 .build();
 
         final Intent clickIntent = new Intent(CLICK_ACTION);
         clickIntent.setClass(mContext, NotificationReceiver.class);
         clickIntent.setData(dataUri);
 
         if (persistentData != null) {
             final Intent persistentIntent = GeckoService.getIntentToCreateServices(
-                    mContext, "persistent-notification-click", persistentData);
+                    "persistent-notification-click", persistentData);
             clickIntent.putExtra(PERSISTENT_INTENT_EXTRA, persistentIntent);
         }
 
         final PendingIntent clickPendingIntent = PendingIntent.getBroadcast(
                 mContext, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
 
         final Intent closeIntent = new Intent(CLOSE_ACTION);
         closeIntent.setClass(mContext, NotificationReceiver.class);
         closeIntent.setData(dataUri);
 
         if (persistentData != null) {
             final Intent persistentIntent = GeckoService.getIntentToCreateServices(
-                    mContext, "persistent-notification-close", persistentData);
+                    "persistent-notification-close", persistentData);
             closeIntent.putExtra(PERSISTENT_INTENT_EXTRA, persistentIntent);
         }
 
         final PendingIntent closePendingIntent = PendingIntent.getBroadcast(
                 mContext, 0, closeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
 
         add(name, imageUrl, host, title, text, clickPendingIntent, closePendingIntent);
         GeckoAppShell.onNotificationShow(name, cookie);
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java
@@ -1,28 +1,29 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.notifications;
 
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoThread;
-import org.mozilla.gecko.mozglue.SafeIntent;
-
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.util.Log;
 
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoServicesCreatorService;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.mozglue.SafeIntent;
+
 /**
  *  Broadcast receiver for Notifications. Will forward them to GeckoApp (and start Gecko) if they're clicked.
  *  If they're being dismissed, it will not start Gecko, but may forward them to JS if Gecko is running.
  *  This is also the only entry point for notification intents.
  */
 public class NotificationReceiver extends BroadcastReceiver {
     private static final String LOGTAG = "GeckoNotificationReceiver";
 
@@ -83,17 +84,17 @@ public class NotificationReceiver extend
                                             final Uri data, final Intent intent) {
         final String name = data.getQueryParameter("name");
         final String cookie = data.getQueryParameter("cookie");
         final Intent persistentIntent = (Intent)
                 intent.getParcelableExtra(NotificationClient.PERSISTENT_INTENT_EXTRA);
 
         if (persistentIntent != null) {
             // Go through GeckoService for persistent notifications.
-            context.startService(persistentIntent);
+            GeckoServicesCreatorService.enqueueWork(context, intent);
         }
 
         if (NotificationClient.CLICK_ACTION.equals(action)) {
             GeckoAppShell.onNotificationClick(name, cookie);
 
             if (persistentIntent != null) {
                 // Don't launch GeckoApp if it's a background persistent notification.
                 return;
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
@@ -14,16 +14,17 @@ import android.support.annotation.NonNul
 import android.util.Log;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoService;
+import org.mozilla.gecko.GeckoServicesCreatorService;
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.annotation.ReflectionTarget;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.devices.FxAccountDeviceRegistrator;
 import org.mozilla.gecko.fxa.FxAccountPushHandler;
@@ -248,19 +249,19 @@ public class PushService implements Bund
         }
 
         if (canSendPushMessagesToGecko()) {
             if (!GeckoThread.canUseProfile(profileName, new File(profilePath))) {
                 Log.e(LOG_TAG, "Mismatched profile for chid: " + chid + "; ignoring dom/push message.");
                 return;
             }
         } else {
-            final Intent intent = GeckoService.getIntentToCreateServices(context, "android-push-service");
+            final Intent intent = GeckoService.getIntentToCreateServices("android-push-service");
             GeckoService.setIntentProfile(intent, profileName, profilePath);
-            context.startService(intent);
+            GeckoServicesCreatorService.enqueueWork(context, intent);
         }
 
         final JSONObject data = new JSONObject();
         try {
             data.put("channelID", chid);
             data.put("con", bundle.getString("con"));
             data.put("enc", bundle.getString("enc"));
             // Only one of cryptokey (newer) and enckey (deprecated) should be set, but the
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceRegistrator.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceRegistrator.java
@@ -2,42 +2,39 @@
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.devices;
 
 import android.content.Context;
 import android.content.Intent;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
 import android.util.Log;
 
+import org.mozilla.gecko.GeckoServicesCreatorService;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.lang.ref.WeakReference;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-import java.security.GeneralSecurityException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
 /* This class provides a way to register the current device against FxA
  * and also stores the registration details in the Android FxAccount.
  * This should be used in a state where we possess a sessionToken, most likely the Engaged/Married states.
  */
 public class FxAccountDeviceRegistrator implements BundleEventListener {
@@ -118,31 +115,30 @@ public class FxAccountDeviceRegistrator 
     }
   }
 
   private void beginRegistration(Context context) {
     // Fire up gecko and send event
     // We create the Intent ourselves instead of using GeckoService.getIntentToCreateServices
     // because we can't import these modules (circular dependency between browser and services)
     final Intent geckoIntent = buildCreatePushServiceIntent(context, "android-fxa-subscribe");
-    context.startService(geckoIntent);
+    GeckoServicesCreatorService.enqueueWork(context, geckoIntent);
     // -> handleMessage()
   }
 
   private void beginRegistrationRenewal(Context context) {
     // Same as registration, but unsubscribe first to get a fresh subscription.
     final Intent geckoIntent = buildCreatePushServiceIntent(context, "android-fxa-resubscribe");
-    context.startService(geckoIntent);
+    GeckoServicesCreatorService.enqueueWork(context, geckoIntent);
     // -> handleMessage()
   }
 
   private Intent buildCreatePushServiceIntent(final Context context, final String data) {
     final Intent intent = new Intent();
     intent.setAction("create-services");
-    intent.setClassName(context, "org.mozilla.gecko.GeckoService");
     intent.putExtra("category", "android-push-service");
     intent.putExtra("data", data);
     final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
     intent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", fxAccount.getProfile());
     return intent;
   }
 
   @Override
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
@@ -8,16 +8,17 @@ import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.v4.app.JobIntentService;
 import android.text.TextUtils;
 
+import org.mozilla.gecko.GeckoServicesCreatorService;
 import org.mozilla.gecko.JobIdsConstants;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClientException;
 import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
 import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
 import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
 import org.mozilla.gecko.db.BrowserContract;
@@ -73,22 +74,21 @@ public class FxAccountDeletedService ext
     clearRemoteDevicesList(intent, context);
 
     // Delete current device the from FxA devices list.
     deleteFxADevice(intent);
 
     // Fire up gecko and unsubscribe push
     final Intent geckoIntent = new Intent();
     geckoIntent.setAction("create-services");
-    geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
     geckoIntent.putExtra("category", "android-push-service");
     geckoIntent.putExtra("data", "android-fxa-unsubscribe");
     geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME",
             intent.getStringExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE));
-    context.startService(geckoIntent);
+    GeckoServicesCreatorService.enqueueWork(context, geckoIntent);
 
     // Delete client database and non-local tabs.
     Logger.info(LOG_TAG, "Deleting the entire Fennec clients database and non-local tabs");
     FennecTabsRepository.deleteNonLocalClientsAndTabs(context);
 
     // For data types which support versioning, we need to reset versions to ensure
     // that all records are be processed whenever sync is connected in the future.
     // See BrowserProvider's call method implementation for details.