Bug 1407046 - Migrate UpdateService to JobIntentService; r?sdaswani draft
authorPetru Lingurar <petru.lingurar@softvision.ro>
Wed, 06 Jun 2018 16:32:41 +0300
changeset 805640 759f528b879703b2ffcfd7140781d1a9562c5569
parent 805639 187af0021ceaf9341fa715f9be5ac9433dd4fd78
push id112722
push userplingurar@mozilla.com
push dateFri, 08 Jun 2018 06:33:31 +0000
reviewerssdaswani
bugs1407046
milestone62.0a1
Bug 1407046 - Migrate UpdateService to JobIntentService; r?sdaswani Broke the big IntentService into four small JobIntentServices because the same JobIntentService class cannot be used with multiple JobIds (https://github.com/aosp-mirror/platform_frameworks_support/blob/b6838fd2d2e834fdd38aab34511d385cb7108f63/compat/src/main/java/android/support/v4/app/JobIntentService.java#L121) Also: - will make the code easier to be migrated to WorkManager in the future - more in line with SRP. It was initially doing too much. All the functionality of the big UpdateService class has been incorporated in Updater.java, UpdatesPrefs.java and UpdatesServiceHelper.java with the main logic to drive the important actions inside the new Services. UpdaterService is used as parent of the newly created service to help avoid duplicated code. Created an inner BroadcastReceiver to act upon notification actions while the service which posted it is running as it's state needed to be modified. Created a BroadcastReceiver to act on actions from notifications which remained posted after the service that posted them finished. This receiver will just start another UpdaterService. Otherwise the services are to be started from the UpdateServiceHelper class. MozReview-Commit-ID: 2OyBZ4YYvgh
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
mobile/android/base/java/org/mozilla/gecko/updater/PackageVerifier.java
mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java
mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java
mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceReceiver.java
mobile/android/base/java/org/mozilla/gecko/updater/Updater.java
mobile/android/base/java/org/mozilla/gecko/updater/UpdaterService.java
mobile/android/base/java/org/mozilla/gecko/updater/UpdatesApplyService.java
mobile/android/base/java/org/mozilla/gecko/updater/UpdatesCheckService.java
mobile/android/base/java/org/mozilla/gecko/updater/UpdatesDownloadService.java
mobile/android/base/java/org/mozilla/gecko/updater/UpdatesPrefs.java
mobile/android/base/java/org/mozilla/gecko/updater/UpdatesRegisterService.java
mobile/android/services/src/main/java/org/mozilla/gecko/background/JobIdsConstants.java
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -370,19 +370,48 @@
                   android:exported="false"/>
 
         <provider android:name="org.mozilla.gecko.db.SearchHistoryProvider"
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.searchhistory"
                   android:exported="false"/>
 
         <service
             android:exported="false"
-            android:name="org.mozilla.gecko.updater.UpdateService"
+            android:name="org.mozilla.gecko.updater.UpdatesApplyService"
+            android:permission="android.permission.BIND_JOB_SERVICE"
+            android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
+        </service>
+        <service
+            android:exported="false"
+            android:name="org.mozilla.gecko.updater.UpdatesCheckService"
+            android:permission="android.permission.BIND_JOB_SERVICE"
+            android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
+        </service>
+        <service
+            android:exported="false"
+            android:name="org.mozilla.gecko.updater.UpdatesDownloadService"
+            android:permission="android.permission.BIND_JOB_SERVICE"
             android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
         </service>
+        <service
+            android:exported="false"
+            android:name="org.mozilla.gecko.updater.UpdatesRegisterService"
+            android:permission="android.permission.BIND_JOB_SERVICE"
+            android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
+        </service>
+
+        <receiver
+            android:name="org.mozilla.gecko.updater.UpdateServiceReceiver"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="@ANDROID_PACKAGE_NAME@.CHECK_FOR_UPDATE"></action>
+                <action android:name="@ANDROID_PACKAGE_NAME@.APPLY_UPDATE"></action>
+                <action android:name="@ANDROID_PACKAGE_NAME@.DOWNLOAD_UPDATE"></action>
+            </intent-filter>
+        </receiver>
 
         <service
             android:exported="false"
             android:name="org.mozilla.gecko.notifications.NotificationService">
         </service>
 
         <service
             android:exported="false"
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -70,18 +70,18 @@ import org.mozilla.gecko.TelemetryContra
 import org.mozilla.gecko.TelemetryContract.Method;
 import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
 import org.mozilla.gecko.mma.MmaDelegate;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.Restrictions;
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabqueue.TabQueuePrompt;
-import org.mozilla.gecko.updater.UpdateService;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
+import org.mozilla.gecko.updater.UpdateServiceHelper.AutoDownloadPolicy;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.ContextUtils;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.InputOptionsUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.ViewUtil;
@@ -1119,17 +1119,17 @@ public class GeckoPreferences
             // Even though this is a list preference, we don't want to handle it
             // below, so we return here.
             return onLocaleSelected(Locales.getLanguageTag(lastLocale), (String) newValue);
         }
 
         if (PREFS_MENU_CHAR_ENCODING.equals(prefName)) {
             setCharEncodingState(((String) newValue).equals("true"));
         } else if (PREFS_UPDATER_AUTODOWNLOAD.equals(prefName)) {
-            UpdateServiceHelper.setAutoDownloadPolicy(this, UpdateService.AutoDownloadPolicy.get((String) newValue));
+            UpdateServiceHelper.setAutoDownloadPolicy(this, AutoDownloadPolicy.get((String) newValue));
         } else if (PREFS_UPDATER_URL.equals(prefName)) {
             UpdateServiceHelper.setUpdateUrl(this, (String) newValue);
         } else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(prefName)) {
             final Boolean newBooleanValue = (Boolean) newValue;
             AdjustConstants.getAdjustHelper().setEnabled(newBooleanValue);
             // If Health Report has been disabled Mma should also be stopped.
             // If it was just enabled, we should also try to start Mma immediately
             // provided that all the other requirements to start Mma are met.
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/PackageVerifier.java
@@ -0,0 +1,67 @@
+package org.mozilla.gecko.updater;
+
+import android.util.Log;
+
+import org.mozilla.apache.commons.codec.binary.Hex;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.MessageDigest;
+
+public class PackageVerifier {
+    private static final String LOGTAG = "PackageVerifier";
+
+    static boolean verifyDownloadedPackage(final UpdatesPrefs prefs, final File updateFile, final int bufSize) {
+        MessageDigest digest = createMessageDigest(prefs.getLastHashFunction());
+        if (digest == null)
+            return false;
+
+        InputStream input = null;
+
+        try {
+            input = new BufferedInputStream(new FileInputStream(updateFile));
+
+            byte[] buf = new byte[bufSize];
+            int len;
+            while ((len = input.read(buf, 0, bufSize)) > 0) {
+                digest.update(buf, 0, len);
+            }
+        } catch (java.io.IOException e) {
+            Log.e(LOGTAG, "Failed to verify update package: ", e);
+            return false;
+        } finally {
+            try {
+                if (input != null)
+                    input.close();
+            } catch (java.io.IOException e) { }
+        }
+
+        String hex = Hex.encodeHexString(digest.digest());
+        if (!hex.equals(prefs.getLastHashValue())) {
+            Log.e(LOGTAG, "Package hash does not match");
+            return false;
+        }
+
+        return true;
+    }
+
+    private static MessageDigest createMessageDigest(String hashFunction) {
+        String javaHashFunction;
+
+        if ("sha512".equalsIgnoreCase(hashFunction)) {
+            javaHashFunction = "SHA-512";
+        } else {
+            Log.e(LOGTAG, "Unhandled hash function: " + hashFunction);
+            return null;
+        }
+
+        try {
+            return MessageDigest.getInstance(javaHashFunction);
+        } catch (java.security.NoSuchAlgorithmException e) {
+            Log.e(LOGTAG, "Couldn't find algorithm " + javaHashFunction, e);
+            return null;
+        }
+    }
+}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java
+++ /dev/null
@@ -1,809 +0,0 @@
-/* -*- 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.updater;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.CrashHandler;
-import org.mozilla.gecko.R;
-
-import org.mozilla.apache.commons.codec.binary.Hex;
-
-import org.mozilla.gecko.permissions.Permissions;
-import org.mozilla.gecko.util.IOUtils;
-import org.mozilla.gecko.util.ProxySelector;
-import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
-import android.Manifest;
-import android.app.AlarmManager;
-import android.app.IntentService;
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.Uri;
-import android.net.wifi.WifiManager;
-import android.net.wifi.WifiManager.WifiLock;
-import android.os.Environment;
-import android.provider.Settings;
-import android.support.v4.app.NotificationManagerCompat;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.net.ConnectivityManagerCompat;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationCompat.Builder;
-import android.util.Log;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.URL;
-import java.net.URLConnection;
-import java.security.MessageDigest;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.TimeZone;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-
-public class UpdateService extends IntentService {
-    private static final int BUFSIZE = 8192;
-    private static final int NOTIFICATION_ID = 0x3e40ddbd;
-
-    private static final String LOGTAG = "UpdateService";
-
-    private static final int INTERVAL_LONG = 86400000; // in milliseconds
-    private static final int INTERVAL_SHORT = 14400000; // again, in milliseconds
-    private static final int INTERVAL_RETRY = 3600000;
-
-    private static final String PREFS_NAME = "UpdateService";
-    private static final String KEY_LAST_BUILDID = "UpdateService.lastBuildID";
-    private static final String KEY_LAST_HASH_FUNCTION = "UpdateService.lastHashFunction";
-    private static final String KEY_LAST_HASH_VALUE = "UpdateService.lastHashValue";
-    private static final String KEY_LAST_FILE_NAME = "UpdateService.lastFileName";
-    private static final String KEY_LAST_ATTEMPT_DATE = "UpdateService.lastAttemptDate";
-    private static final String KEY_AUTODOWNLOAD_POLICY = "UpdateService.autoDownloadPolicy";
-    private static final String KEY_UPDATE_URL = "UpdateService.updateUrl";
-
-    private SharedPreferences mPrefs;
-
-    private NotificationManagerCompat mNotificationManager;
-    private ConnectivityManager mConnectivityManager;
-    private Builder mBuilder;
-
-    private volatile WifiLock mWifiLock;
-
-    private boolean mDownloading;
-    private boolean mCancelDownload;
-    private boolean mApplyImmediately;
-
-    private CrashHandler mCrashHandler;
-
-    public enum AutoDownloadPolicy {
-        NONE(-1),
-        WIFI(0),
-        DISABLED(1),
-        ENABLED(2);
-
-        public final int value;
-
-        private AutoDownloadPolicy(int value) {
-            this.value = value;
-        }
-
-        private final static AutoDownloadPolicy[] sValues = AutoDownloadPolicy.values();
-
-        public static AutoDownloadPolicy get(int value) {
-            for (AutoDownloadPolicy id: sValues) {
-                if (id.value == value) {
-                    return id;
-                }
-            }
-            return NONE;
-        }
-
-        public static AutoDownloadPolicy get(String name) {
-            for (AutoDownloadPolicy id: sValues) {
-                if (name.equalsIgnoreCase(id.toString())) {
-                    return id;
-                }
-            }
-            return NONE;
-        }
-    }
-
-    private enum CheckUpdateResult {
-        // Keep these in sync with mobile/android/chrome/content/about.xhtml
-        NOT_AVAILABLE,
-        AVAILABLE,
-        DOWNLOADING,
-        DOWNLOADED
-    }
-
-
-    public UpdateService() {
-        super("updater");
-    }
-
-    @Override
-    public void onCreate () {
-        mCrashHandler = CrashHandler.createDefaultCrashHandler(getApplicationContext());
-
-        super.onCreate();
-
-        mPrefs = getSharedPreferences(PREFS_NAME, 0);
-        mNotificationManager = NotificationManagerCompat.from(this);
-        mConnectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-        mWifiLock = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE))
-                    .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, PREFS_NAME);
-        mCancelDownload = false;
-    }
-
-    @Override
-    public void onDestroy() {
-        mCrashHandler.unregister();
-        mCrashHandler = null;
-
-        if (mWifiLock.isHeld()) {
-            mWifiLock.release();
-        }
-    }
-
-    @Override
-    public synchronized int onStartCommand (Intent intent, int flags, int startId) {
-        // If we are busy doing a download, the new Intent here would normally be queued for
-        // execution once that is done. In this case, however, we want to flip the boolean
-        // while that is running, so handle that now.
-        if (mDownloading && UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) {
-            Log.i(LOGTAG, "will apply update when download finished");
-
-            mApplyImmediately = true;
-            showDownloadNotification();
-        } else if (UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD.equals(intent.getAction())) {
-            mCancelDownload = true;
-        } else {
-            super.onStartCommand(intent, flags, startId);
-        }
-
-        return Service.START_REDELIVER_INTENT;
-    }
-
-    @Override
-    protected void onHandleIntent (final Intent intent) {
-        if (UpdateServiceHelper.ACTION_REGISTER_FOR_UPDATES.equals(intent.getAction())) {
-            AutoDownloadPolicy policy = AutoDownloadPolicy.get(
-                intent.getIntExtra(UpdateServiceHelper.EXTRA_AUTODOWNLOAD_NAME,
-                                   AutoDownloadPolicy.NONE.value));
-
-            if (policy != AutoDownloadPolicy.NONE) {
-                setAutoDownloadPolicy(policy);
-            }
-
-            String url = intent.getStringExtra(UpdateServiceHelper.EXTRA_UPDATE_URL_NAME);
-            if (url != null) {
-                setUpdateUrl(url);
-            }
-
-            registerForUpdates(false);
-        } else if (UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE.equals(intent.getAction())) {
-            startUpdate(intent.getIntExtra(UpdateServiceHelper.EXTRA_UPDATE_FLAGS_NAME, 0));
-            // Use this instead for forcing a download from about:fennec
-            // startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD | UpdateServiceHelper.FLAG_REINSTALL);
-        } else if (UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE.equals(intent.getAction())) {
-            // We always want to do the download and apply it here
-            mApplyImmediately = true;
-            startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD);
-        } else if (UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) {
-            applyUpdate(intent.getStringExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME));
-        }
-    }
-
-    private static boolean hasFlag(int flags, int flag) {
-        return (flags & flag) == flag;
-    }
-
-    private void sendCheckUpdateResult(CheckUpdateResult result) {
-        Intent resultIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT);
-        resultIntent.putExtra("result", result.toString());
-        sendBroadcast(resultIntent);
-    }
-
-    private int getUpdateInterval(boolean isRetry) {
-        int interval;
-        if (isRetry) {
-            interval = INTERVAL_RETRY;
-        } else if (!AppConstants.RELEASE_OR_BETA) {
-            interval = INTERVAL_SHORT;
-        } else {
-            interval = INTERVAL_LONG;
-        }
-
-        return interval;
-    }
-
-    private void registerForUpdates(boolean isRetry) {
-        Calendar lastAttempt = getLastAttemptDate();
-        Calendar now = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
-
-        int interval = getUpdateInterval(isRetry);
-
-        if (lastAttempt == null || (now.getTimeInMillis() - lastAttempt.getTimeInMillis()) > interval) {
-            // We've either never attempted an update, or we are passed the desired
-            // time. Start an update now.
-            Log.i(LOGTAG, "no update has ever been attempted, checking now");
-            startUpdate(0);
-            return;
-        }
-
-        AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-        if (manager == null)
-            return;
-
-        PendingIntent pending = PendingIntent.getService(this, 0, new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE, null, this, UpdateService.class), PendingIntent.FLAG_UPDATE_CURRENT);
-        manager.cancel(pending);
-
-        lastAttempt.setTimeInMillis(lastAttempt.getTimeInMillis() + interval);
-        Log.i(LOGTAG, "next update will be at: " + lastAttempt.getTime());
-
-        manager.set(AlarmManager.RTC_WAKEUP, lastAttempt.getTimeInMillis(), pending);
-    }
-
-    private void startUpdate(final int flags) {
-        setLastAttemptDate();
-
-        NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo();
-        if (netInfo == null || !netInfo.isConnected()) {
-            Log.i(LOGTAG, "not connected to the network");
-            registerForUpdates(true);
-            sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
-            return;
-        }
-
-        registerForUpdates(false);
-
-        final UpdateInfo info = findUpdate(hasFlag(flags, UpdateServiceHelper.FLAG_REINSTALL));
-        boolean haveUpdate = (info != null);
-
-        if (!haveUpdate) {
-            Log.i(LOGTAG, "no update available");
-            sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
-            return;
-        }
-
-        Log.i(LOGTAG, "update available, buildID = " + info.buildID);
-
-        Permissions.from(this)
-                .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
-                .doNotPrompt()
-                .andFallback(new Runnable() {
-                    @Override
-                    public void run() {
-                        showPermissionNotification();
-                        sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
-                    }
-                })
-                .run(new Runnable() {
-                    @Override
-                    public void run() {
-                        startDownload(info, flags);
-                    }
-                });
-    }
-
-    private void startDownload(UpdateInfo info, int flags) {
-        AutoDownloadPolicy policy = getAutoDownloadPolicy();
-
-        // We only start a download automatically if one of following criteria are met:
-        //
-        // - We have a FORCE_DOWNLOAD flag passed in
-        // - The preference is set to 'always'
-        // - The preference is set to 'wifi' and we are using a non-metered network (i.e. the user
-        //   is OK with large data transfers occurring)
-        //
-        boolean shouldStartDownload = hasFlag(flags, UpdateServiceHelper.FLAG_FORCE_DOWNLOAD) ||
-                policy == AutoDownloadPolicy.ENABLED ||
-                (policy == AutoDownloadPolicy.WIFI && !ConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager));
-
-        if (!shouldStartDownload) {
-            Log.i(LOGTAG, "not initiating automatic update download due to policy " + policy.toString());
-            sendCheckUpdateResult(CheckUpdateResult.AVAILABLE);
-
-            // We aren't autodownloading here, so prompt to start the update
-            Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE);
-            notificationIntent.setClass(this, UpdateService.class);
-            PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
-            NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
-            builder.setSmallIcon(R.drawable.ic_status_logo);
-            builder.setWhen(System.currentTimeMillis());
-            builder.setAutoCancel(true);
-            builder.setContentTitle(getString(R.string.updater_start_title));
-            builder.setContentText(getString(R.string.updater_start_select));
-            builder.setContentIntent(contentIntent);
-
-            mNotificationManager.notify(NOTIFICATION_ID, builder.build());
-
-            return;
-        }
-
-        File pkg = downloadUpdatePackage(info, hasFlag(flags, UpdateServiceHelper.FLAG_OVERWRITE_EXISTING));
-        if (pkg == null) {
-            sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
-            return;
-        }
-
-        Log.i(LOGTAG, "have update package at " + pkg);
-
-        saveUpdateInfo(info, pkg);
-        sendCheckUpdateResult(CheckUpdateResult.DOWNLOADED);
-
-        if (mApplyImmediately) {
-            applyUpdate(pkg);
-        } else {
-            // Prompt to apply the update
-
-            Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE);
-            notificationIntent.setClass(this, UpdateService.class);
-            notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, pkg.getAbsolutePath());
-            PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
-
-            NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
-            builder.setSmallIcon(R.drawable.ic_status_logo);
-            builder.setWhen(System.currentTimeMillis());
-            builder.setAutoCancel(true);
-            builder.setContentTitle(getString(R.string.updater_apply_title));
-            builder.setContentText(getString(R.string.updater_apply_select));
-            builder.setContentIntent(contentIntent);
-
-            mNotificationManager.notify(NOTIFICATION_ID, builder.build());
-        }
-    }
-
-    private UpdateInfo findUpdate(boolean force) {
-        URLConnection conn = null;
-        try {
-            URI uri = getUpdateURI(force);
-
-            if (uri == null) {
-              Log.e(LOGTAG, "failed to get update URI");
-              return null;
-            }
-
-            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
-            conn = ProxySelector.openConnectionWithProxy(uri);
-            Document dom = builder.parse(conn.getInputStream());
-
-            NodeList nodes = dom.getElementsByTagName("update");
-            if (nodes == null || nodes.getLength() == 0)
-                return null;
-
-            Node updateNode = nodes.item(0);
-            Node buildIdNode = updateNode.getAttributes().getNamedItem("buildID");
-            if (buildIdNode == null)
-                return null;
-
-            nodes = dom.getElementsByTagName("patch");
-            if (nodes == null || nodes.getLength() == 0)
-                return null;
-
-            Node patchNode = nodes.item(0);
-            Node urlNode = patchNode.getAttributes().getNamedItem("URL");
-            Node hashFunctionNode = patchNode.getAttributes().getNamedItem("hashFunction");
-            Node hashValueNode = patchNode.getAttributes().getNamedItem("hashValue");
-            Node sizeNode = patchNode.getAttributes().getNamedItem("size");
-
-            if (urlNode == null || hashFunctionNode == null ||
-                hashValueNode == null || sizeNode == null) {
-                return null;
-            }
-
-            // Fill in UpdateInfo from the XML data
-            UpdateInfo info = new UpdateInfo();
-            info.uri = new URI(urlNode.getTextContent());
-            info.buildID = buildIdNode.getTextContent();
-            info.hashFunction = hashFunctionNode.getTextContent();
-            info.hashValue = hashValueNode.getTextContent();
-
-            try {
-                info.size = Integer.parseInt(sizeNode.getTextContent());
-            } catch (NumberFormatException e) {
-                Log.e(LOGTAG, "Failed to find APK size: ", e);
-                return null;
-            }
-
-            // Make sure we have all the stuff we need to apply the update
-            if (!info.isValid()) {
-                Log.e(LOGTAG, "missing some required update information, have: " + info);
-                return null;
-            }
-
-            return info;
-        } catch (Exception e) {
-            Log.e(LOGTAG, "failed to check for update: ", e);
-            return null;
-        } finally {
-            // conn isn't guaranteed to be an HttpURLConnection, hence we don't want to cast earlier
-            // in this method. However in our current implementation it usually is, so we need to
-            // make sure we close it in that case:
-            final HttpURLConnection httpConn = (HttpURLConnection) conn;
-            if (httpConn != null) {
-                httpConn.disconnect();
-            }
-        }
-    }
-
-    private MessageDigest createMessageDigest(String hashFunction) {
-        String javaHashFunction = null;
-
-        if ("sha512".equalsIgnoreCase(hashFunction)) {
-            javaHashFunction = "SHA-512";
-        } else {
-            Log.e(LOGTAG, "Unhandled hash function: " + hashFunction);
-            return null;
-        }
-
-        try {
-            return MessageDigest.getInstance(javaHashFunction);
-        } catch (java.security.NoSuchAlgorithmException e) {
-            Log.e(LOGTAG, "Couldn't find algorithm " + javaHashFunction, e);
-            return null;
-        }
-    }
-
-    private void showDownloadNotification() {
-        showDownloadNotification(null);
-    }
-
-    private void showDownloadNotification(File downloadFile) {
-
-        Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE);
-        notificationIntent.setClass(this, UpdateService.class);
-
-        Intent cancelIntent = new Intent(UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD);
-        cancelIntent.setClass(this, UpdateService.class);
-
-        if (downloadFile != null)
-            notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, downloadFile.getAbsolutePath());
-
-        PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-        PendingIntent deleteIntent = PendingIntent.getService(this, 0, cancelIntent, PendingIntent.FLAG_CANCEL_CURRENT);
-
-        mBuilder = new NotificationCompat.Builder(this);
-        mBuilder.setContentTitle(getResources().getString(R.string.updater_downloading_title))
-                .setContentText(mApplyImmediately ? "" : getResources().getString(R.string.updater_downloading_select))
-                .setSmallIcon(android.R.drawable.stat_sys_download)
-                .setContentIntent(contentIntent)
-                .setDeleteIntent(deleteIntent);
-
-        mBuilder.setProgress(100, 0, true);
-        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
-    }
-
-    private void showDownloadFailure() {
-        Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE);
-        notificationIntent.setClass(this, UpdateService.class);
-        PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
-        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
-        builder.setSmallIcon(R.drawable.ic_status_logo);
-        builder.setWhen(System.currentTimeMillis());
-        builder.setContentTitle(getString(R.string.updater_downloading_title_failed));
-        builder.setContentText(getString(R.string.updater_downloading_retry));
-        builder.setContentIntent(contentIntent);
-
-        mNotificationManager.notify(NOTIFICATION_ID, builder.build());
-    }
-
-    private boolean deleteUpdatePackage(String path) {
-        if (path == null) {
-            return false;
-        }
-
-        File pkg = new File(path);
-        if (!pkg.exists()) {
-            return false;
-        }
-
-        pkg.delete();
-        Log.i(LOGTAG, "deleted update package: " + path);
-
-        return true;
-    }
-
-    private File downloadUpdatePackage(UpdateInfo info, boolean overwriteExisting) {
-        URL url = null;
-        try {
-            url = info.uri.toURL();
-        } catch (java.net.MalformedURLException e) {
-            Log.e(LOGTAG, "failed to read URL: ", e);
-            return null;
-        }
-
-        File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
-        path.mkdirs();
-        String fileName = new File(url.getFile()).getName();
-        File downloadFile = new File(path, fileName);
-
-        if (!overwriteExisting && info.buildID.equals(getLastBuildID()) && downloadFile.exists()) {
-            // The last saved buildID is the same as the one for the current update. We also have a file
-            // already downloaded, so it's probably the package we want. Verify it to be sure and just
-            // return that if it matches.
-
-            if (verifyDownloadedPackage(downloadFile)) {
-                Log.i(LOGTAG, "using existing update package");
-                return downloadFile;
-            } else {
-                // Didn't match, so we're going to download a new one.
-                downloadFile.delete();
-            }
-        }
-
-        if (!info.buildID.equals(getLastBuildID())) {
-            // Delete the previous package when a new version becomes available.
-            deleteUpdatePackage(getLastFileName());
-        }
-
-        Log.i(LOGTAG, "downloading update package");
-        sendCheckUpdateResult(CheckUpdateResult.DOWNLOADING);
-
-        OutputStream output = null;
-        InputStream input = null;
-        URLConnection conn = null;
-
-        mDownloading = true;
-        mCancelDownload = false;
-        showDownloadNotification(downloadFile);
-
-        try {
-            NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo();
-            if (netInfo != null && netInfo.isConnected() &&
-                netInfo.getType() == ConnectivityManager.TYPE_WIFI) {
-                mWifiLock.acquire();
-            }
-
-            conn = ProxySelector.openConnectionWithProxy(info.uri);
-            int length = conn.getContentLength();
-
-            output = new BufferedOutputStream(new FileOutputStream(downloadFile));
-            input = new BufferedInputStream(conn.getInputStream());
-
-            byte[] buf = new byte[BUFSIZE];
-            int len = 0;
-
-            int bytesRead = 0;
-            int lastNotify = 0;
-
-            while ((len = input.read(buf, 0, BUFSIZE)) > 0 && !mCancelDownload) {
-                output.write(buf, 0, len);
-                bytesRead += len;
-                // Updating the notification takes time so only do it every 1MB
-                if (bytesRead - lastNotify > 1048576) {
-                    mBuilder.setProgress(length, bytesRead, false);
-                    mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
-                    lastNotify = bytesRead;
-                }
-            }
-
-            mNotificationManager.cancel(NOTIFICATION_ID);
-
-            // if the download was canceled by the user
-            // delete the update package
-            if (mCancelDownload) {
-                Log.i(LOGTAG, "download canceled by user!");
-                downloadFile.delete();
-
-                return null;
-            } else {
-                Log.i(LOGTAG, "completed update download!");
-                return downloadFile;
-            }
-        } catch (Exception e) {
-            downloadFile.delete();
-            showDownloadFailure();
-
-            Log.e(LOGTAG, "failed to download update: ", e);
-            return null;
-        } finally {
-            IOUtils.safeStreamClose(input);
-            IOUtils.safeStreamClose(output);
-
-            mDownloading = false;
-
-            if (mWifiLock.isHeld()) {
-                mWifiLock.release();
-            }
-
-            // conn isn't guaranteed to be an HttpURLConnection, hence we don't want to cast earlier
-            // in this method. However in our current implementation it usually is, so we need to
-            // make sure we close it in that case:
-            final HttpURLConnection httpConn = (HttpURLConnection) conn;
-            if (httpConn != null) {
-                httpConn.disconnect();
-            }
-        }
-    }
-
-    private boolean verifyDownloadedPackage(File updateFile) {
-        MessageDigest digest = createMessageDigest(getLastHashFunction());
-        if (digest == null)
-            return false;
-
-        InputStream input = null;
-
-        try {
-            input = new BufferedInputStream(new FileInputStream(updateFile));
-
-            byte[] buf = new byte[BUFSIZE];
-            int len;
-            while ((len = input.read(buf, 0, BUFSIZE)) > 0) {
-                digest.update(buf, 0, len);
-            }
-        } catch (java.io.IOException e) {
-            Log.e(LOGTAG, "Failed to verify update package: ", e);
-            return false;
-        } finally {
-            try {
-                if (input != null)
-                    input.close();
-            } catch (java.io.IOException e) { }
-        }
-
-        String hex = Hex.encodeHexString(digest.digest());
-        if (!hex.equals(getLastHashValue())) {
-            Log.e(LOGTAG, "Package hash does not match");
-            return false;
-        }
-
-        return true;
-    }
-
-    private void applyUpdate(String updatePath) {
-        if (updatePath == null) {
-            updatePath = getLastFileName();
-        }
-
-        if (updatePath != null) {
-            applyUpdate(new File(updatePath));
-        }
-    }
-
-    private void applyUpdate(File updateFile) {
-        mApplyImmediately = false;
-
-        if (!updateFile.exists())
-            return;
-
-        Log.i(LOGTAG, "Verifying package: " + updateFile);
-
-        if (!verifyDownloadedPackage(updateFile)) {
-            Log.e(LOGTAG, "Not installing update, failed verification");
-            return;
-        }
-
-        Intent intent = new Intent(Intent.ACTION_VIEW);
-        intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive");
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        startActivity(intent);
-    }
-
-    private void showPermissionNotification() {
-        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
-                Uri.fromParts("package", getPackageName(), null));
-
-        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
-
-        NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle()
-                .bigText(getString(R.string.updater_permission_text));
-
-        Notification notification = new NotificationCompat.Builder(this)
-                .setContentTitle(getString(R.string.updater_permission_title))
-                .setContentText(getString(R.string.updater_permission_text))
-                .setStyle(bigTextStyle)
-                .setAutoCancel(true)
-                .setSmallIcon(R.drawable.ic_status_logo)
-                .setColor(ContextCompat.getColor(this, R.color.rejection_red))
-                .setContentIntent(pendingIntent)
-                .build();
-
-        NotificationManagerCompat.from(this)
-                .notify(R.id.updateServicePermissionNotification, notification);
-    }
-
-    private String getLastBuildID() {
-        return mPrefs.getString(KEY_LAST_BUILDID, null);
-    }
-
-    private String getLastHashFunction() {
-        return mPrefs.getString(KEY_LAST_HASH_FUNCTION, null);
-    }
-
-    private String getLastHashValue() {
-        return mPrefs.getString(KEY_LAST_HASH_VALUE, null);
-    }
-
-    private String getLastFileName() {
-        return mPrefs.getString(KEY_LAST_FILE_NAME, null);
-    }
-
-    private Calendar getLastAttemptDate() {
-        long lastAttempt = mPrefs.getLong(KEY_LAST_ATTEMPT_DATE, -1);
-        if (lastAttempt < 0)
-            return null;
-
-        GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
-        cal.setTimeInMillis(lastAttempt);
-        return cal;
-    }
-
-    private void setLastAttemptDate() {
-        SharedPreferences.Editor editor = mPrefs.edit();
-        editor.putLong(KEY_LAST_ATTEMPT_DATE, System.currentTimeMillis());
-        editor.commit();
-    }
-
-    private AutoDownloadPolicy getAutoDownloadPolicy() {
-        return AutoDownloadPolicy.get(mPrefs.getInt(KEY_AUTODOWNLOAD_POLICY, AutoDownloadPolicy.WIFI.value));
-    }
-
-    private void setAutoDownloadPolicy(AutoDownloadPolicy policy) {
-        SharedPreferences.Editor editor = mPrefs.edit();
-        editor.putInt(KEY_AUTODOWNLOAD_POLICY, policy.value);
-        editor.commit();
-    }
-
-    private URI getUpdateURI(boolean force) {
-        return UpdateServiceHelper.expandUpdateURI(this, mPrefs.getString(KEY_UPDATE_URL, null), force);
-    }
-
-    private void setUpdateUrl(String url) {
-        SharedPreferences.Editor editor = mPrefs.edit();
-        editor.putString(KEY_UPDATE_URL, url);
-        editor.commit();
-    }
-
-    private void saveUpdateInfo(UpdateInfo info, File downloaded) {
-        SharedPreferences.Editor editor = mPrefs.edit();
-        editor.putString(KEY_LAST_BUILDID, info.buildID);
-        editor.putString(KEY_LAST_HASH_FUNCTION, info.hashFunction);
-        editor.putString(KEY_LAST_HASH_VALUE, info.hashValue);
-        editor.putString(KEY_LAST_FILE_NAME, downloaded.toString());
-        editor.commit();
-    }
-
-    private static final class UpdateInfo {
-        public URI uri;
-        public String buildID;
-        public String hashFunction;
-        public String hashValue;
-        public int size;
-
-        private boolean isNonEmpty(String s) {
-            return s != null && s.length() > 0;
-        }
-
-        public boolean isValid() {
-            return uri != null && isNonEmpty(buildID) &&
-                isNonEmpty(hashFunction) && isNonEmpty(hashValue) && size > 0;
-        }
-
-        @Override
-        public String toString() {
-            return "uri = " + uri + ", buildID = " + buildID + ", hashFunction = " + hashFunction + ", hashValue = " + hashValue + ", size = " + size;
-        }
-    }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java
@@ -1,46 +1,70 @@
 /* -*- 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.updater;
 
-import org.mozilla.gecko.annotation.RobocopTarget;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v4.app.JobIntentService;
+import android.util.Log;
+
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.background.JobIdsConstants;
 import org.mozilla.gecko.util.ContextUtils;
 import org.mozilla.gecko.util.GeckoJarReader;
 
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ApplicationInfo;
-import android.os.Build;
-import android.util.Log;
-
 import java.net.URI;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Map;
 
 public class UpdateServiceHelper {
+    // Following can be used to start a specific UpdaterService
     public static final String ACTION_REGISTER_FOR_UPDATES = AppConstants.ANDROID_PACKAGE_NAME + ".REGISTER_FOR_UPDATES";
-    public static final String ACTION_UNREGISTER_FOR_UPDATES = AppConstants.ANDROID_PACKAGE_NAME + ".UNREGISTER_FOR_UPDATES";
     public static final String ACTION_CHECK_FOR_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_FOR_UPDATE";
-    public static final String ACTION_CHECK_UPDATE_RESULT = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_UPDATE_RESULT";
     public static final String ACTION_DOWNLOAD_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".DOWNLOAD_UPDATE";
     public static final String ACTION_APPLY_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".APPLY_UPDATE";
-    public static final String ACTION_CANCEL_DOWNLOAD = AppConstants.ANDROID_PACKAGE_NAME + ".CANCEL_DOWNLOAD";
+
+    // Used to inform Gecko about current status
+    public static final String ACTION_CHECK_UPDATE_RESULT = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_UPDATE_RESULT"; //
+    // Used to change running service state
+    public static final String ACTION_CANCEL_DOWNLOAD = AppConstants.ANDROID_PACKAGE_NAME + ".CANCEL_DOWNLOAD";     //
 
-    // Flags for ACTION_CHECK_FOR_UPDATE
-    protected static final int FLAG_FORCE_DOWNLOAD = 1;
-    protected static final int FLAG_OVERWRITE_EXISTING = 1 << 1;
-    protected static final int FLAG_REINSTALL = 1 << 2;
-    protected static final int FLAG_RETRY = 1 << 3;
+    // Easy way to get the specific JobId for each JobIntentService depending on the desired action
+    private static Map<String, Integer> serviceActionJobIds;
+    static {
+        Map<String, Integer> map = new HashMap<>(6);
+        map.put(ACTION_REGISTER_FOR_UPDATES, JobIdsConstants.JOB_ID_UPDATES_REGISTER);
+        map.put(ACTION_CHECK_FOR_UPDATE, JobIdsConstants.JOB_ID_UPDATES_CHECK_FOR);
+        map.put(ACTION_DOWNLOAD_UPDATE, JobIdsConstants.JOB_ID_UPDATES_DOWNLOAD);
+        map.put(ACTION_APPLY_UPDATE, JobIdsConstants.JOB_ID_UPDATES_APPLY);
+        serviceActionJobIds = Collections.unmodifiableMap(map);
+    }
+
+    // Update intervals
+    private static final int INTERVAL_LONG = 1000 * 60 * 60 * 24; // 1 day in milliseconds
+    private static final int INTERVAL_SHORT = 1000 * 60 * 60 * 4; // 4 hours in milliseconds
+    private static final int INTERVAL_RETRY = 1000 * 60 * 60 * 1; // 1 hour in milliseconds
+
+    // The number of bytes to read when working with the updated package
+    static final int BUFSIZE = 8192;
+
+    // All the notifications posted by the update services will use the same id
+    static final int NOTIFICATION_ID = 0x3e40ddbd;
 
     // Name of the Intent extra for the autodownload policy, used with ACTION_REGISTER_FOR_UPDATES
     protected static final String EXTRA_AUTODOWNLOAD_NAME = "autodownload";
 
     // Name of the Intent extra that holds the flags for ACTION_CHECK_FOR_UPDATE
     protected static final String EXTRA_UPDATE_FLAGS_NAME = "updateFlags";
 
     // Name of the Intent extra that holds the APK path, used with ACTION_APPLY_UPDATE
@@ -50,27 +74,90 @@ public class UpdateServiceHelper {
     protected static final String EXTRA_UPDATE_URL_NAME = "updateUrl";
 
     private static final String LOGTAG = "UpdateServiceHelper";
     private static final String DEFAULT_UPDATE_LOCALE = "en-US";
 
     // So that updates can be disabled by tests.
     private static volatile boolean isEnabled = true;
 
+    static final class UpdateInfo {
+        public URI uri;
+        public String buildID;
+        public String hashFunction;
+        public String hashValue;
+        public int size;
+
+        private boolean isNonEmpty(String s) {
+            return s != null && s.length() > 0;
+        }
+
+        public boolean isValid() {
+            return uri != null && isNonEmpty(buildID) &&
+                    isNonEmpty(hashFunction) && isNonEmpty(hashValue) && size > 0;
+        }
+
+        @Override
+        public String toString() {
+            return "uri = " + uri + ", buildID = " + buildID + ", hashFunction = " + hashFunction + ", hashValue = " + hashValue + ", size = " + size;
+        }
+    }
+
+    public enum AutoDownloadPolicy {
+        NONE(-1),
+        WIFI(0),
+        DISABLED(1),
+        ENABLED(2);
+
+        public final int value;
+
+        AutoDownloadPolicy(int value) {
+            this.value = value;
+        }
+
+        private final static AutoDownloadPolicy[] sValues = AutoDownloadPolicy.values();
+
+        public static AutoDownloadPolicy get(int value) {
+            for (AutoDownloadPolicy id: sValues) {
+                if (id.value == value) {
+                    return id;
+                }
+            }
+            return NONE;
+        }
+
+        public static AutoDownloadPolicy get(String name) {
+            for (AutoDownloadPolicy id: sValues) {
+                if (name.equalsIgnoreCase(id.toString())) {
+                    return id;
+                }
+            }
+            return NONE;
+        }
+    }
+
+    enum CheckUpdateResult {
+        // Keep these in sync with mobile/android/chrome/content/about.xhtml
+        NOT_AVAILABLE,
+        AVAILABLE,
+        DOWNLOADING,
+        DOWNLOADED
+    }
+
     private enum Pref {
         AUTO_DOWNLOAD_POLICY("app.update.autodownload"),
         UPDATE_URL("app.update.url.android");
 
-        public final String name;
+        final String name;
 
-        private Pref(String name) {
+        Pref(String name) {
             this.name = name;
         }
 
-        public final static String[] names;
+        final static String[] names;
 
         @Override
         public String toString() {
             return this.name;
         }
 
         static {
             ArrayList<String> nameList = new ArrayList<String>();
@@ -135,79 +222,108 @@ public class UpdateServiceHelper {
     public static boolean isUpdaterEnabled(final Context context) {
         return AppConstants.MOZ_UPDATER && isEnabled && !ContextUtils.isInstalledFromGooglePlay(context);
     }
 
     public static void setUpdateUrl(Context context, String url) {
         registerForUpdates(context, null, url);
     }
 
-    public static void setAutoDownloadPolicy(Context context, UpdateService.AutoDownloadPolicy policy) {
+    public static void setAutoDownloadPolicy(Context context, AutoDownloadPolicy policy) {
         registerForUpdates(context, policy, null);
     }
 
     public static void checkForUpdate(Context context) {
         if (context == null) {
             return;
         }
 
-        context.startService(createIntent(context, ACTION_CHECK_FOR_UPDATE));
+        enqueueUpdateCheck(context, new Intent());
     }
 
     public static void downloadUpdate(Context context) {
         if (context == null) {
             return;
         }
 
-        context.startService(createIntent(context, ACTION_DOWNLOAD_UPDATE));
+        enqueueUpdateDownload(context, new Intent());
     }
 
     public static void applyUpdate(Context context) {
         if (context == null) {
             return;
         }
 
-        context.startService(createIntent(context, ACTION_APPLY_UPDATE));
+        enqueueUpdateApply(context, new Intent());
     }
 
     public static void registerForUpdates(final Context context) {
         if (!isUpdaterEnabled(context)) {
              return;
         }
 
         final HashMap<String, Object> prefs = new HashMap<String, Object>();
 
         PrefsHelper.getPrefs(Pref.names, new PrefsHelper.PrefHandlerBase() {
             @Override public void prefValue(String pref, String value) {
                 prefs.put(pref, value);
             }
 
             @Override public void finish() {
                 UpdateServiceHelper.registerForUpdates(context,
-                    UpdateService.AutoDownloadPolicy.get(
+                    AutoDownloadPolicy.get(
                         (String) prefs.get(Pref.AUTO_DOWNLOAD_POLICY.toString())),
                       (String) prefs.get(Pref.UPDATE_URL.toString()));
             }
         });
     }
 
-    public static void registerForUpdates(Context context, UpdateService.AutoDownloadPolicy policy, String url) {
+    private static void registerForUpdates(Context context, AutoDownloadPolicy policy, String url) {
         if (!isUpdaterEnabled(context)) {
              return;
         }
 
-        Intent intent = createIntent(context, ACTION_REGISTER_FOR_UPDATES);
+        Intent intent = new Intent(ACTION_REGISTER_FOR_UPDATES);
 
         if (policy != null) {
             intent.putExtra(EXTRA_AUTODOWNLOAD_NAME, policy.value);
         }
 
         if (url != null) {
             intent.putExtra(EXTRA_UPDATE_URL_NAME, url);
         }
 
-        context.startService(intent);
+        enqueueUpdateRegister(context, intent);
+    }
+
+    static int getUpdateInterval(boolean isRetry) {
+        int interval;
+        if (isRetry) {
+            interval = INTERVAL_RETRY;
+        } else if (!AppConstants.RELEASE_OR_BETA) {
+            interval = INTERVAL_SHORT;
+        } else {
+            interval = INTERVAL_LONG;
+        }
+
+        return interval;
     }
 
-    private static Intent createIntent(Context context, String action) {
-        return new Intent(action, null, context, UpdateService.class);
+    public static void enqueueUpdateApply(@NonNull final Context context, @NonNull final Intent workIntent) {
+        JobIntentService.enqueueWork(context, UpdatesApplyService.class,
+                JobIdsConstants.JOB_ID_UPDATES_APPLY, workIntent);
+    }
+
+    public static void enqueueUpdateCheck(@NonNull final Context context, @NonNull final Intent workIntent) {
+        JobIntentService.enqueueWork(context, UpdatesCheckService.class,
+                JobIdsConstants.JOB_ID_UPDATES_CHECK_FOR, workIntent);
+    }
+
+    public static void enqueueUpdateDownload(@NonNull final Context context, @NonNull final Intent workIntent) {
+        JobIntentService.enqueueWork(context, UpdatesDownloadService.class,
+                JobIdsConstants.JOB_ID_UPDATES_DOWNLOAD, workIntent);
+    }
+
+    public static void enqueueUpdateRegister(@NonNull final Context context, @NonNull final Intent workIntent) {
+        JobIntentService.enqueueWork(context, UpdatesRegisterService.class,
+                JobIdsConstants.JOB_ID_UPDATES_REGISTER, workIntent);
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceReceiver.java
@@ -0,0 +1,38 @@
+package org.mozilla.gecko.updater;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * Receiver only used in local notifications posted by an {@link UpdaterService} after which it finishes.<br>
+ * Used to start another variant of {@link UpdaterService}.
+ */
+public class UpdateServiceReceiver extends BroadcastReceiver {
+    private static final String LOGTAG = "UpdateServiceReceiver";
+
+    public UpdateServiceReceiver() {
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        final String action = intent.getAction();
+
+        Log.d(LOGTAG, String.format("Will enqueue \"%s\" work", action));
+
+        switch (action) {
+            case UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE:
+                UpdateServiceHelper.enqueueUpdateCheck(context, intent);
+                break;
+            case UpdateServiceHelper.ACTION_APPLY_UPDATE:
+                UpdateServiceHelper.enqueueUpdateApply(context, intent);
+                break;
+            case UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE:
+                UpdateServiceHelper.enqueueUpdateDownload(context, intent);
+                break;
+            default:
+                // no-op, we only listen for the above
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/Updater.java
@@ -0,0 +1,620 @@
+package org.mozilla.gecko.updater;
+
+import android.Manifest;
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.WifiLock;
+import android.os.Environment;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.net.ConnectivityManagerCompat;
+import android.util.Log;
+
+import org.mozilla.gecko.GeckoUpdateReceiver;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.permissions.Permissions;
+import org.mozilla.gecko.updater.UpdateServiceHelper.AutoDownloadPolicy;
+import org.mozilla.gecko.updater.UpdateServiceHelper.CheckUpdateResult;
+import org.mozilla.gecko.updater.UpdateServiceHelper.UpdateInfo;
+import org.mozilla.gecko.util.IOUtils;
+import org.mozilla.gecko.util.ProxySelector;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+/**
+ * Helper class that handles everything needed to download and apply an updated package.<br>
+ * Only to be used on background threads because of the long time running operations involved.
+ */
+public class Updater {
+    // Because this class can be called by multiple parties the log tag
+    // will actually be the caller's name to easily identify whom requested the operations.
+    private String logtag;
+
+    // Flags for ACTION_CHECK_FOR_UPDATE
+    static final int FLAG_FORCE_DOWNLOAD = 1;
+    static final int FLAG_OVERWRITE_EXISTING = 1 << 1;
+    static final int FLAG_REINSTALL = 1 << 2;
+
+    private final Context context;
+    private final ConnectivityManager connectivityManager;
+    private final NotificationManagerCompat notificationManager;
+    private final int bufferSize;
+    private final int notificationId;
+    private volatile WifiLock mWifiLock;
+
+    private NotificationCompat.Builder notifBuilder;
+    private BroadcastReceiver innerBroadcastReceiver;
+    private UpdatesPrefs prefs;
+
+    private boolean shouldCancelDownload;
+    private boolean shouldApplyImmediately;
+
+    Updater(final Context context, final UpdatesPrefs prefs, final int bufSize, final int notificationId) {
+        this.logtag = getLogtagFromParentName(context);
+        this.context = context;
+        this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        this.notificationManager = NotificationManagerCompat.from(context);
+        this.prefs = prefs;
+        this.mWifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE))
+                .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, prefs.getPreferenceName());
+        this.bufferSize = bufSize;
+        this.notificationId = notificationId;
+
+        registerForLocalBroadcasts(context);
+
+        Log.d(logtag, "Updater created");
+    }
+
+    /**
+     * Must be called everytime the caller finished it's work to cleanup used resources.
+     */
+    void finish() {
+        unregisterFromLocalBroadcasts();
+
+        if (mWifiLock.isHeld()) {
+            mWifiLock.release();
+        }
+    }
+
+    void setIfShouldApplyUpdateImmediately(boolean shouldApplyImmediately) {
+        this.shouldApplyImmediately = shouldApplyImmediately;
+    }
+
+    void startUpdate(final int flags) {
+        prefs.setLastAttemptDate();
+
+        NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
+        if (netInfo == null || !netInfo.isConnected()) {
+            Log.i(logtag, "not connected to the network");
+            registerForUpdates(true);
+            sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
+            return;
+        }
+
+        registerForUpdates(false);
+
+        final UpdateInfo info = findUpdate(hasFlag(flags, FLAG_REINSTALL));
+        boolean haveUpdate = (info != null);
+
+        if (!haveUpdate) {
+            Log.i(logtag, "no update available");
+            sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
+            return;
+        }
+
+        Log.i(logtag, "update available, buildID = " + info.buildID);
+
+        Permissions.from(context)
+                .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+                .doNotPrompt()
+                .andFallback(new Runnable() {
+                    @Override
+                    public void run() {
+                        showPermissionNotification();
+                        sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
+                    }
+                })
+                .run(new Runnable() {
+                    @Override
+                    public void run() {
+                        startDownload(info, flags);
+                    }
+                });
+
+    }
+
+    void registerForUpdates(boolean isRetry) {
+        Calendar lastAttempt = prefs.getLastAttemptDate();
+        Calendar now = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+
+        int interval = UpdateServiceHelper.getUpdateInterval(isRetry);
+
+        if (lastAttempt == null || (now.getTimeInMillis() - lastAttempt.getTimeInMillis()) > interval) {
+            // We've either never attempted an update, or we are passed the desired
+            // time. Start an update now.
+            Log.i(logtag, "no update has ever been attempted, checking now");
+            startUpdate(0);
+            return;
+        }
+
+        AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        if (manager == null)
+            return;
+
+        PendingIntent pending = PendingIntent.getBroadcast(context, 0,
+                new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE, null, context, UpdateServiceReceiver.class),
+                PendingIntent.FLAG_UPDATE_CURRENT);
+        manager.cancel(pending);
+
+        lastAttempt.setTimeInMillis(lastAttempt.getTimeInMillis() + interval);
+        Log.i(logtag, "next update will be at: " + lastAttempt.getTime());
+
+        manager.set(AlarmManager.RTC_WAKEUP, lastAttempt.getTimeInMillis(), pending);
+    }
+
+     void applyUpdate(String updatePath) {
+        if (updatePath == null) {
+            updatePath = prefs.getLastFileName();
+        }
+
+        if (updatePath != null) {
+            applyUpdate(new File(updatePath));
+        }
+    }
+
+    private void showPermissionNotification() {
+        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+                Uri.fromParts("package", context.getPackageName(), null));
+
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+
+        NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle()
+                .bigText(getString(R.string.updater_permission_text));
+
+        Notification notification = new NotificationCompat.Builder(context)
+                .setContentTitle(getString(R.string.updater_permission_title))
+                .setContentText(getString(R.string.updater_permission_text))
+                .setStyle(bigTextStyle)
+                .setAutoCancel(true)
+                .setSmallIcon(R.drawable.ic_status_logo)
+                .setColor(ContextCompat.getColor(context, R.color.rejection_red))
+                .setContentIntent(pendingIntent)
+                .build();
+
+        NotificationManagerCompat.from(context)
+                .notify(R.id.updateServicePermissionNotification, notification);
+    }
+
+    private void startDownload(UpdateInfo info, int flags) {
+        AutoDownloadPolicy policy = prefs.getAutoDownloadPolicy();
+
+        // We only start a download automatically if one of following criteria are met:
+        //
+        // - We have a FORCE_DOWNLOAD flag passed in
+        // - The preference is set to 'always'
+        // - The preference is set to 'wifi' and we are using a non-metered network (i.e. the user
+        //   is OK with large data transfers occurring)
+        //
+        boolean shouldStartDownload = hasFlag(flags, FLAG_FORCE_DOWNLOAD) ||
+                policy == AutoDownloadPolicy.ENABLED ||
+                (policy == AutoDownloadPolicy.WIFI && !ConnectivityManagerCompat.isActiveNetworkMetered(connectivityManager));
+
+        if (!shouldStartDownload) {
+            Log.i(logtag, "not initiating automatic update download due to policy " + policy.toString());
+            sendCheckUpdateResult(CheckUpdateResult.AVAILABLE);
+
+            // We aren't autodownloading here, so prompt to start the update with a new JobIntentService
+            Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE);
+            notificationIntent.setClass(context, UpdateServiceReceiver.class);
+            PendingIntent contentIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+            NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+            builder.setSmallIcon(R.drawable.ic_status_logo);
+            builder.setWhen(System.currentTimeMillis());
+            builder.setAutoCancel(true);
+            builder.setContentTitle(getString(R.string.updater_start_title));
+            builder.setContentText(getString(R.string.updater_start_select));
+            builder.setContentIntent(contentIntent);
+
+            notificationManager.notify(notificationId, builder.build());
+
+            return;
+        }
+
+        File pkg = downloadUpdatePackage(info, hasFlag(flags, FLAG_OVERWRITE_EXISTING));
+        if (pkg == null) {
+            sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
+            return;
+        }
+
+        Log.i(logtag, "have update package at " + pkg);
+
+        prefs.saveUpdateInfo(info, pkg);
+        sendCheckUpdateResult(CheckUpdateResult.DOWNLOADED);
+
+        if (shouldApplyImmediately) {
+            applyUpdate(pkg);
+        } else {
+            // Prompt to apply the update with a new JobIntentService
+
+            Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE);
+            notificationIntent.setClass(context, UpdateServiceReceiver.class);
+            notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, pkg.getAbsolutePath());
+            PendingIntent contentIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+
+            NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+            builder.setSmallIcon(R.drawable.ic_status_logo);
+            builder.setWhen(System.currentTimeMillis());
+            builder.setAutoCancel(true);
+            builder.setContentTitle(getString(R.string.updater_apply_title));
+            builder.setContentText(getString(R.string.updater_apply_select));
+            builder.setContentIntent(contentIntent);
+
+            notificationManager.notify(notificationId, builder.build());
+        }
+    }
+
+    private File downloadUpdatePackage(UpdateInfo info, boolean overwriteExisting) {
+        URL url = null;
+        try {
+            url = info.uri.toURL();
+        } catch (java.net.MalformedURLException e) {
+            Log.e(logtag, "failed to read URL: ", e);
+            return null;
+        }
+
+        File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+        path.mkdirs();
+        String fileName = new File(url.getFile()).getName();
+        File downloadFile = new File(path, fileName);
+
+
+        final String lastBuildId = prefs.getLastBuildID();
+        if (!overwriteExisting && info.buildID.equals(lastBuildId) && downloadFile.exists()) {
+            // The last saved buildID is the same as the one for the current update. We also have a file
+            // already downloaded, so it's probably the package we want. Verify it to be sure and just
+            // return that if it matches.
+
+            if (PackageVerifier.verifyDownloadedPackage(prefs, downloadFile, bufferSize)) {
+                Log.i(logtag, "using existing update package");
+                return downloadFile;
+            } else {
+                // Didn't match, so we're going to download a new one.
+                downloadFile.delete();
+            }
+        }
+
+        if (!info.buildID.equals(prefs.getLastBuildID())) {
+            // Delete the previous package when a new version becomes available.
+            deleteUpdatePackage(prefs.getLastFileName());
+        }
+
+        Log.i(logtag, "downloading update package");
+        sendCheckUpdateResult(CheckUpdateResult.DOWNLOADING);
+
+        OutputStream output = null;
+        InputStream input = null;
+        URLConnection conn = null;
+
+        shouldCancelDownload = false;
+        showDownloadNotification(downloadFile);
+
+        try {
+            NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
+            if (netInfo != null && netInfo.isConnected() &&
+                    netInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+                mWifiLock.acquire();
+            }
+
+            conn = ProxySelector.openConnectionWithProxy(info.uri);
+            int length = conn.getContentLength();
+
+            output = new BufferedOutputStream(new FileOutputStream(downloadFile));
+            input = new BufferedInputStream(conn.getInputStream());
+
+            byte[] buf = new byte[bufferSize];
+            int len = 0;
+
+            int bytesRead = 0;
+            int lastNotify = 0;
+
+            while ((len = input.read(buf, 0, bufferSize)) > 0 && !shouldCancelDownload) {
+                output.write(buf, 0, len);
+                bytesRead += len;
+                // Updating the notification takes time so only do it every 1MB
+                if (bytesRead - lastNotify > 1048576) {
+                    notifBuilder.setProgress(length, bytesRead, false);
+                    notificationManager.notify(notificationId, notifBuilder.build());
+                    lastNotify = bytesRead;
+                }
+            }
+
+            notificationManager.cancel(notificationId);
+
+            // if the download was canceled by the user
+            // delete the update package
+            if (shouldCancelDownload) {
+                Log.i(logtag, "download canceled by user!");
+                downloadFile.delete();
+
+                return null;
+            } else {
+                Log.i(logtag, "completed update download!");
+                return downloadFile;
+            }
+        } catch (Exception e) {
+            downloadFile.delete();
+            showDownloadFailure();
+
+            Log.e(logtag, "failed to download update: ", e);
+            return null;
+        } finally {
+            IOUtils.safeStreamClose(input);
+            IOUtils.safeStreamClose(output);
+
+            if (mWifiLock.isHeld()) {
+                mWifiLock.release();
+            }
+
+            // conn isn't guaranteed to be an HttpURLConnection, hence we don't want to cast earlier
+            // in this method. However in our current implementation it usually is, so we need to
+            // make sure we close it in that case:
+            final HttpURLConnection httpConn = (HttpURLConnection) conn;
+            if (httpConn != null) {
+                httpConn.disconnect();
+            }
+        }
+    }
+
+    private void showDownloadNotification() {
+        showDownloadNotification(null);
+    }
+
+    private void showDownloadNotification(File downloadFile) {
+
+        Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE);
+        notificationIntent.setClass(context, UserUpdatesReceiver.class);
+
+        Intent cancelIntent = new Intent(UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD);
+        cancelIntent.setClass(context, UserUpdatesReceiver.class);
+
+        if (downloadFile != null)
+            notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, downloadFile.getAbsolutePath());
+
+        PendingIntent contentIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+        notifBuilder = new NotificationCompat.Builder(context);
+        notifBuilder.setContentTitle(getString(R.string.updater_downloading_title))
+                .setContentText(shouldApplyImmediately ? "" : getString(R.string.updater_downloading_select))
+                .setSmallIcon(android.R.drawable.stat_sys_download)
+                .setContentIntent(contentIntent)
+                .setDeleteIntent(deleteIntent);
+
+        notifBuilder.setProgress(100, 0, true);
+        notificationManager.notify(notificationId, notifBuilder.build());
+    }
+
+    private void showDownloadFailure() {
+        // Let the user restart the update process with a new JobIntentService
+        Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE);
+        notificationIntent.setClass(context, UpdateServiceReceiver.class);
+        PendingIntent contentIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+        builder.setSmallIcon(R.drawable.ic_status_logo);
+        builder.setWhen(System.currentTimeMillis());
+        builder.setContentTitle(getString(R.string.updater_downloading_title_failed));
+        builder.setContentText(getString(R.string.updater_downloading_retry));
+        builder.setContentIntent(contentIntent);
+
+        notificationManager.notify(notificationId, builder.build());
+    }
+
+    private void applyUpdate(File updateFile) {
+        shouldApplyImmediately = false;
+
+        if (!updateFile.exists())
+            return;
+
+        Log.i(logtag, "Verifying package: " + updateFile);
+
+        if (!PackageVerifier.verifyDownloadedPackage(prefs, updateFile, bufferSize)) {
+            Log.e(logtag, "Not installing update, failed verification");
+            return;
+        }
+
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive");
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivity(intent);
+    }
+
+    private boolean deleteUpdatePackage(String path) {
+        if (path == null) {
+            return false;
+        }
+
+        File pkg = new File(path);
+        if (!pkg.exists()) {
+            return false;
+        }
+
+        pkg.delete();
+        Log.i(logtag, "deleted update package: " + path);
+
+        return true;
+    }
+
+    private void sendCheckUpdateResult(CheckUpdateResult result) {
+        Intent resultIntent = new Intent(context, GeckoUpdateReceiver.class);
+        resultIntent.setAction(UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT);
+        resultIntent.putExtra("result", result.toString());
+        context.sendBroadcast(resultIntent);
+    }
+
+    private UpdateInfo findUpdate(boolean force) {
+        URLConnection conn = null;
+        try {
+            URI uri = prefs.getUpdateURI(force);
+
+            if (uri == null) {
+                Log.e(logtag, "failed to get update URI");
+                return null;
+            }
+
+            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+            conn = ProxySelector.openConnectionWithProxy(uri);
+            Document dom = builder.parse(conn.getInputStream());
+
+            NodeList nodes = dom.getElementsByTagName("update");
+            if (nodes == null || nodes.getLength() == 0)
+                return null;
+
+            Node updateNode = nodes.item(0);
+            Node buildIdNode = updateNode.getAttributes().getNamedItem("buildID");
+            if (buildIdNode == null)
+                return null;
+
+            nodes = dom.getElementsByTagName("patch");
+            if (nodes == null || nodes.getLength() == 0)
+                return null;
+
+            Node patchNode = nodes.item(0);
+            Node urlNode = patchNode.getAttributes().getNamedItem("URL");
+            Node hashFunctionNode = patchNode.getAttributes().getNamedItem("hashFunction");
+            Node hashValueNode = patchNode.getAttributes().getNamedItem("hashValue");
+            Node sizeNode = patchNode.getAttributes().getNamedItem("size");
+
+            if (urlNode == null || hashFunctionNode == null ||
+                    hashValueNode == null || sizeNode == null) {
+                return null;
+            }
+
+            // Fill in UpdateInfo from the XML data
+            UpdateInfo info = new UpdateInfo();
+            info.uri = new URI(urlNode.getTextContent());
+            info.buildID = buildIdNode.getTextContent();
+            info.hashFunction = hashFunctionNode.getTextContent();
+            info.hashValue = hashValueNode.getTextContent();
+
+            try {
+                info.size = Integer.parseInt(sizeNode.getTextContent());
+            } catch (NumberFormatException e) {
+                Log.e(logtag, "Failed to find APK size: ", e);
+                return null;
+            }
+
+            // Make sure we have all the stuff we need to apply the update
+            if (!info.isValid()) {
+                Log.e(logtag, "missing some required update information, have: " + info);
+                return null;
+            }
+
+            return info;
+        } catch (Exception e) {
+            Log.e(logtag, "failed to check for update: ", e);
+            return null;
+        } finally {
+            // conn isn't guaranteed to be an HttpURLConnection, hence we don't want to cast earlier
+            // in this method. However in our current implementation it usually is, so we need to
+            // make sure we close it in that case:
+            final HttpURLConnection httpConn = (HttpURLConnection) conn;
+            if (httpConn != null) {
+                httpConn.disconnect();
+            }
+        }
+    }
+
+    private String getString(@StringRes final int stringResId) {
+        return context.getString(stringResId);
+    }
+
+    private static boolean hasFlag(int flags, int flag) {
+        return (flags & flag) == flag;
+    }
+
+    // The log tag can have at most 23 chars or Log will throw an IllegalArgumentException
+    private String getLogtagFromParentName(@NonNull final Context context) {
+        final String parentName = context.getClass().getSimpleName();
+        if (parentName.length() > 23) {
+            return parentName.substring(0, 24);
+        } else {
+            return parentName;
+        }
+    }
+
+    /**
+     * Receiver only used in local notifications that allow for actions to modify current running state.
+     * As such it is an inner class of current JobIntentService.<br>
+     * For any notifications that could allow for actions after the service completed it's work
+     * use {@link UpdateServiceReceiver}
+     */
+    private class UserUpdatesReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+
+            switch (action) {
+                case UpdateServiceHelper.ACTION_APPLY_UPDATE:
+                    // User can only press a notification button for this while a download is in progress.
+                    // If download finished the notification will be update to allow starting
+                    // a new JobIntentService to apply the update
+                    Log.i(logtag, "will apply update when download finished");
+                    shouldApplyImmediately = true;
+                    showDownloadNotification();
+                    break;
+                case UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD:
+                    shouldCancelDownload = true;
+                    break;
+                default:
+                    // no-op, we only listen for the above
+            }
+        }
+    }
+
+    private void registerForLocalBroadcasts(@NonNull final Context context) {
+        innerBroadcastReceiver = new UserUpdatesReceiver();
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(UpdateServiceHelper.ACTION_APPLY_UPDATE);
+        intentFilter.addAction(UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD);
+
+        context.registerReceiver(innerBroadcastReceiver, intentFilter);
+    }
+
+    private void unregisterFromLocalBroadcasts() {
+        context.unregisterReceiver(innerBroadcastReceiver);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdaterService.java
@@ -0,0 +1,33 @@
+package org.mozilla.gecko.updater;
+
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.support.v4.app.JobIntentService;
+
+import org.mozilla.gecko.CrashHandler;
+
+public abstract class UpdaterService extends JobIntentService {
+    private CrashHandler crashHandler;
+    protected Updater updater;
+    protected UpdatesPrefs prefs;
+
+    @Override
+    public void onCreate() {
+        crashHandler = CrashHandler.createDefaultCrashHandler(getApplicationContext());
+
+        super.onCreate();
+
+        prefs = new UpdatesPrefs(this);
+        updater = new Updater(this, prefs, UpdateServiceHelper.BUFSIZE, UpdateServiceHelper.NOTIFICATION_ID);
+    }
+
+    @Override
+    protected abstract void onHandleWork(@NonNull Intent intent);
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        updater.finish();
+        crashHandler.unregister();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdatesApplyService.java
@@ -0,0 +1,13 @@
+package org.mozilla.gecko.updater;
+
+import android.content.Intent;
+import android.support.annotation.NonNull;
+
+public class UpdatesApplyService extends UpdaterService {
+    @Override
+    protected void onHandleWork(@NonNull Intent intent) {
+        // The apply action from the notification while the download is in progress is
+        // treated in the inner BroadcastReceiver of the service which posted the notification
+        updater.applyUpdate(intent.getStringExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME));
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdatesCheckService.java
@@ -0,0 +1,14 @@
+package org.mozilla.gecko.updater;
+
+import android.content.Intent;
+import android.support.annotation.NonNull;
+
+public class UpdatesCheckService extends UpdaterService {
+    @Override
+    protected void onHandleWork(@NonNull Intent intent) {
+        updater.startUpdate(intent.getIntExtra(UpdateServiceHelper.EXTRA_UPDATE_FLAGS_NAME, 0));
+
+        // Use this instead for forcing a download from about:fennec
+        // startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD | UpdateServiceHelper.FLAG_REINSTALL);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdatesDownloadService.java
@@ -0,0 +1,12 @@
+package org.mozilla.gecko.updater;
+
+import android.content.Intent;
+import android.support.annotation.NonNull;
+
+public class UpdatesDownloadService extends UpdaterService {
+    @Override
+    protected void onHandleWork(@NonNull Intent intent) {
+        updater.setIfShouldApplyUpdateImmediately(true);
+        updater.startUpdate(Updater.FLAG_FORCE_DOWNLOAD);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdatesPrefs.java
@@ -0,0 +1,98 @@
+package org.mozilla.gecko.updater;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.updater.UpdateServiceHelper.AutoDownloadPolicy;
+import org.mozilla.gecko.updater.UpdateServiceHelper.UpdateInfo;
+
+import java.io.File;
+import java.net.URI;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+public final class UpdatesPrefs {
+    private static final String PREFS_NAME = "UpdateService";
+    private static final String KEY_LAST_BUILDID = "UpdateService.lastBuildID";
+    private static final String KEY_LAST_HASH_FUNCTION = "UpdateService.lastHashFunction";
+    private static final String KEY_LAST_HASH_VALUE = "UpdateService.lastHashValue";
+    private static final String KEY_LAST_FILE_NAME = "UpdateService.lastFileName";
+    private static final String KEY_LAST_ATTEMPT_DATE = "UpdateService.lastAttemptDate";
+    private static final String KEY_AUTODOWNLOAD_POLICY = "UpdateService.autoDownloadPolicy";
+    private static final String KEY_UPDATE_URL = "UpdateService.updateUrl";
+
+    private SharedPreferences sharedPrefs;
+
+    UpdatesPrefs(@NonNull final Context context) {
+        sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+    }
+
+    String getPreferenceName() {
+        return PREFS_NAME;
+    }
+
+    String getLastBuildID() {
+        return sharedPrefs.getString(KEY_LAST_BUILDID, null);
+    }
+
+    String getLastHashFunction() {
+        return sharedPrefs.getString(KEY_LAST_HASH_FUNCTION, null);
+    }
+
+    String getLastHashValue() {
+        return sharedPrefs.getString(KEY_LAST_HASH_VALUE, null);
+    }
+
+    String getLastFileName() {
+        return sharedPrefs.getString(KEY_LAST_FILE_NAME, null);
+    }
+
+    Calendar getLastAttemptDate() {
+        long lastAttempt = sharedPrefs.getLong(KEY_LAST_ATTEMPT_DATE, -1);
+        if (lastAttempt < 0)
+            return null;
+
+        GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+        cal.setTimeInMillis(lastAttempt);
+        return cal;
+    }
+
+    void setLastAttemptDate() {
+        SharedPreferences.Editor editor = sharedPrefs.edit();
+        editor.putLong(KEY_LAST_ATTEMPT_DATE, System.currentTimeMillis());
+        editor.commit();
+    }
+
+    AutoDownloadPolicy getAutoDownloadPolicy() {
+        return AutoDownloadPolicy.get(sharedPrefs.getInt(KEY_AUTODOWNLOAD_POLICY, AutoDownloadPolicy.WIFI.value));
+    }
+
+    void setAutoDownloadPolicy(AutoDownloadPolicy policy) {
+        SharedPreferences.Editor editor = sharedPrefs.edit();
+        editor.putInt(KEY_AUTODOWNLOAD_POLICY, policy.value);
+        editor.commit();
+    }
+
+    URI getUpdateURI(boolean force) {
+        return UpdateServiceHelper.expandUpdateURI(GeckoAppShell.getApplicationContext(),
+                sharedPrefs.getString(KEY_UPDATE_URL, null), force);
+    }
+
+    void setUpdateUrl(String url) {
+        SharedPreferences.Editor editor = sharedPrefs.edit();
+        editor.putString(KEY_UPDATE_URL, url);
+        editor.commit();
+    }
+
+    void saveUpdateInfo(UpdateInfo info, File downloaded) {
+        SharedPreferences.Editor editor = sharedPrefs.edit();
+        editor.putString(KEY_LAST_BUILDID, info.buildID);
+        editor.putString(KEY_LAST_HASH_FUNCTION, info.hashFunction);
+        editor.putString(KEY_LAST_HASH_VALUE, info.hashValue);
+        editor.putString(KEY_LAST_FILE_NAME, downloaded.toString());
+        editor.commit();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdatesRegisterService.java
@@ -0,0 +1,26 @@
+package org.mozilla.gecko.updater;
+
+import android.content.Intent;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.updater.UpdateServiceHelper.AutoDownloadPolicy;
+
+public class UpdatesRegisterService extends UpdaterService {
+    @Override
+    protected void onHandleWork(@NonNull Intent intent) {
+        AutoDownloadPolicy policy = AutoDownloadPolicy.get(
+                intent.getIntExtra(UpdateServiceHelper.EXTRA_AUTODOWNLOAD_NAME,
+                        AutoDownloadPolicy.NONE.value));
+
+        if (policy != AutoDownloadPolicy.NONE) {
+            prefs.setAutoDownloadPolicy(policy);
+        }
+
+        String url = intent.getStringExtra(UpdateServiceHelper.EXTRA_UPDATE_URL_NAME);
+        if (url != null) {
+            prefs.setUpdateUrl(url);
+        }
+
+        updater.registerForUpdates(false);
+    }
+}
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/JobIdsConstants.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/JobIdsConstants.java
@@ -17,9 +17,14 @@ public class JobIdsConstants {
     public static final int JOB_ID_TAB_RECEIVED = -5;
 
     public static final int JOB_ID_PROFILE_FETCH = -6;
     public static final int JOB_ID_PROFILE_DELETE = -7;
 
     public static final int JOB_ID_TELEMETRY_UPLOAD = -8;
 
     public static final int JOB_ID_FILE_CLEANUP = -9;
+
+    public static final int JOB_ID_UPDATES_REGISTER = -10;
+    public static final int JOB_ID_UPDATES_CHECK_FOR = -11;
+    public static final int JOB_ID_UPDATES_DOWNLOAD = -12;
+    public static final int JOB_ID_UPDATES_APPLY = -13;
 }