diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2e9d78a04..7d2503634 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -258,6 +258,10 @@
         </receiver>
 
         <service android:name=".UpdateService"/>
+        <service
+                android:name=".UpdateJobService"
+                android:exported="false"
+                android:permission="android.permission.BIND_JOB_SERVICE"/>
         <service
                 android:name=".net.DownloaderService"
                 android:exported="false"/>
diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java
index 228e9f960..4dc49628e 100644
--- a/app/src/main/java/org/fdroid/fdroid/Preferences.java
+++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java
@@ -75,6 +75,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
     private static final boolean DEFAULT_SHOW_INCOMPAT_VERSIONS = false;
     private static final boolean DEFAULT_SHOW_ROOT_APPS = true;
     private static final boolean DEFAULT_SHOW_ANTI_FEATURE_APPS = true;
+    private static final int DEFAULT_UPD_INTERVAL = 24;
     private static final boolean DEFAULT_PRIVILEGED_INSTALLER = true;
     //private static final boolean DEFAULT_LOCAL_REPO_BONJOUR = true;
     private static final long DEFAULT_KEEP_CACHE_TIME = TimeUnit.DAYS.toMillis(1);
@@ -157,6 +158,21 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
      */
     private static final String PREF_CACHE_APK = "cacheDownloaded";
 
+    /**
+     * Get the update interval in milliseconds.
+     */
+    public int getUpdateInterval() {
+        int hours;
+        try {
+            String value = preferences.getString(PREF_UPD_INTERVAL,
+                    String.valueOf(DEFAULT_UPD_INTERVAL));
+            hours = Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            hours = DEFAULT_UPD_INTERVAL;
+        }
+        return hours * 60 * 60 * 1000;
+    }
+
     /**
      * Time in millis to keep cached files.  Anything that has been around longer will be deleted
      */
diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateJobService.java b/app/src/main/java/org/fdroid/fdroid/UpdateJobService.java
new file mode 100644
index 000000000..73561ce19
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/UpdateJobService.java
@@ -0,0 +1,43 @@
+package org.fdroid.fdroid;
+
+import android.annotation.TargetApi;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.Intent;
+
+/**
+ * Interface between the new {@link android.app.job.JobScheduler} API and
+ * our old {@link UpdateService}, which is based on {@link android.app.IntentService}.
+ * This does not do things the way it should, e.g. stopping the job on
+ * {@link #onStopJob(JobParameters)} and properly reporting
+ * {@link #jobFinished(JobParameters, boolean)}, but this at least provides
+ * the nice early triggering when there is good power/wifi available.
+ *
+ * @see <a href="https://developer.android.com/about/versions/android-5.0.html#Power">Project Volta: Scheduling jobs</a>
+ */
+@TargetApi(21)
+public class UpdateJobService extends JobService {
+    @Override
+    public boolean onStartJob(final JobParameters params) {
+        new Thread() {
+            @Override
+            public void run() {
+                // faking the actually run time
+                try {
+                    startService(new Intent(UpdateJobService.this, UpdateService.class));
+                    Thread.sleep(2000);
+                } catch (InterruptedException e) {
+                    // ignored
+                } finally {
+                    jobFinished(params, false);
+                }
+            }
+        }.start();
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        return false;
+    }
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java
index 20360a5f8..73f7591cd 100644
--- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java
+++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java
@@ -22,11 +22,15 @@ import android.app.AlarmManager;
 import android.app.IntentService;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
+import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
@@ -51,6 +55,7 @@ import org.fdroid.fdroid.net.BluetoothDownloader;
 import org.fdroid.fdroid.net.ConnectivityMonitorService;
 import org.fdroid.fdroid.views.main.MainActivity;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -116,32 +121,41 @@ public class UpdateService extends IntentService {
     }
 
     /**
-     * Schedule or cancel this service to update the app index, according to the
-     * current preferences. Should be called a) at boot, b) if the preference
-     * is changed, or c) on startup, in case we get upgraded.
+     * Schedule this service to update the app index while canceling any previously
+     * scheduled updates, according to the current preferences. Should be called
+     * a) at boot, b) if the preference is changed, or c) on startup, in case we get
+     * upgraded. It works differently on {@code android-21} and newer, versus older,
+     * due to the {@link JobScheduler} API handling it very nicely for us.
+     *
+     * @see <a href="https://developer.android.com/about/versions/android-5.0.html#Power">Project Volta: Scheduling jobs</a>
      */
-    public static void schedule(Context ctx) {
+    public static void schedule(Context context) {
+        int interval = Preferences.get().getUpdateInterval();
 
-        SharedPreferences prefs = PreferenceManager
-                .getDefaultSharedPreferences(ctx);
-        String sint = prefs.getString(Preferences.PREF_UPD_INTERVAL, "0");
-        int interval = Integer.parseInt(sint);
+        if (Build.VERSION.SDK_INT < 21) {
+            Intent intent = new Intent(context, UpdateService.class);
+            PendingIntent pending = PendingIntent.getService(context, 0, intent, 0);
 
-        Intent intent = new Intent(ctx, UpdateService.class);
-        PendingIntent pending = PendingIntent.getService(ctx, 0, intent, 0);
-
-        AlarmManager alarm = (AlarmManager) ctx
-                .getSystemService(Context.ALARM_SERVICE);
-        alarm.cancel(pending);
-        if (interval > 0) {
-            alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
-                    SystemClock.elapsedRealtime() + 5000,
-                    AlarmManager.INTERVAL_HOUR, pending);
-            Utils.debugLog(TAG, "Update scheduler alarm set");
+            AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+            alarm.cancel(pending);
+            if (interval > 0) {
+                alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
+                        SystemClock.elapsedRealtime() + 5000, interval, pending);
+                Utils.debugLog(TAG, "Update scheduler alarm set");
+            } else {
+                Utils.debugLog(TAG, "Update scheduler alarm not set");
+            }
         } else {
-            Utils.debugLog(TAG, "Update scheduler alarm not set");
+            Utils.debugLog(TAG, "Using android-21 JobScheduler for updates");
+            JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+            jobScheduler.cancelAll();
+            ComponentName componentName = new ComponentName(context, UpdateJobService.class);
+            JobInfo task = new JobInfo.Builder(0xfedcba, componentName)
+                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
+                    .setOverrideDeadline(interval)
+                    .build();
+            jobScheduler.schedule(task);
         }
-
     }
 
     /**
@@ -152,6 +166,51 @@ public class UpdateService extends IntentService {
         return updating;
     }
 
+    private static volatile boolean isScheduleIfStillOnWifiRunning;
+
+    /**
+     * Waits for a period of time for the WiFi to settle, then if the WiFi is
+     * still active, it schedules an update.  This is to encourage the use of
+     * unlimited networks over metered networks for index updates and auto
+     * downloads of app updates. Starting with {@code android-21}, this uses
+     * {@link android.app.job.JobScheduler} instead.
+     */
+    public static void scheduleIfStillOnWifi(Context context) {
+        if (Build.VERSION.SDK_INT >= 21) {
+            throw new IllegalStateException("This should never be used on android-21 or newer!");
+        }
+        if (isScheduleIfStillOnWifiRunning || !Preferences.get().isBackgroundDownloadAllowed()) {
+            return;
+        }
+        isScheduleIfStillOnWifiRunning = true;
+        new StillOnWifiAsyncTask(context).execute();
+    }
+
+    private static final class StillOnWifiAsyncTask extends AsyncTask<Void, Void, Void> {
+
+        private final WeakReference<Context> contextWeakReference;
+
+        private StillOnWifiAsyncTask(Context context) {
+            this.contextWeakReference = new WeakReference<>(context);
+        }
+
+        @Override
+        protected Void doInBackground(Void... voids) {
+            Context context = contextWeakReference.get();
+            try {
+                Thread.sleep(120000);
+                if (Preferences.get().isBackgroundDownloadAllowed()) {
+                    Utils.debugLog(TAG, "scheduling update because there is good internet");
+                    schedule(context);
+                }
+            } catch (Exception e) {
+                Utils.debugLog(TAG, e.getMessage());
+            }
+            isScheduleIfStillOnWifiRunning = false;
+            return null;
+        }
+    }
+
     @Override
     public void onCreate() {
         super.onCreate();
@@ -279,35 +338,6 @@ public class UpdateService extends IntentService {
         }
     };
 
-    /**
-     * Check whether it is time to run the scheduled update.
-     * We don't want to run if:
-     * - The time between scheduled runs is set to zero (though don't know
-     * when that would occur)
-     * - Last update was too recent
-     * - Not on wifi, but the property for "Only auto update on wifi" is set.
-     *
-     * @return True if we are due for a scheduled update.
-     */
-    private boolean verifyIsTimeForScheduledRun() {
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
-        String sint = prefs.getString(Preferences.PREF_UPD_INTERVAL, "0");
-        int interval = Integer.parseInt(sint);
-        if (interval == 0) {
-            Log.i(TAG, "Skipping update - disabled");
-            return false;
-        }
-        long lastUpdate = prefs.getLong(STATE_LAST_UPDATED, 0);
-        long elapsed = System.currentTimeMillis() - lastUpdate;
-        if (elapsed < interval * 60 * 60 * 1000) {
-            Log.i(TAG, "Skipping update - done " + elapsed
-                    + "ms ago, interval is " + interval + " hours");
-            return false;
-        }
-
-        return true;
-    }
-
     /**
      * In order to send a {@link Toast} from a {@link IntentService}, we have to do these tricks.
      */
@@ -339,6 +369,7 @@ public class UpdateService extends IntentService {
         }
 
         try {
+            final Preferences fdroidPrefs = Preferences.get();
             // See if it's time to actually do anything yet...
             int netState = ConnectivityMonitorService.getNetworkState(this);
             if (address != null && address.startsWith(BluetoothDownloader.SCHEME)) {
@@ -349,12 +380,9 @@ public class UpdateService extends IntentService {
                     sendNoInternetToast();
                 }
                 return;
-            }
-
-            final Preferences fdroidPrefs = Preferences.get();
-            if (manualUpdate || forcedUpdate) {
+            } else if (manualUpdate || forcedUpdate) {
                 Utils.debugLog(TAG, "manually requested or forced update");
-            } else if (!verifyIsTimeForScheduledRun() || !fdroidPrefs.isBackgroundDownloadAllowed()) {
+            } else if (!fdroidPrefs.isBackgroundDownloadAllowed()) {
                 Utils.debugLog(TAG, "don't run update");
                 return;
             }
diff --git a/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java b/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java
index c4280c3cc..cdc99524f 100644
--- a/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java
+++ b/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java
@@ -8,6 +8,7 @@ import android.net.DhcpInfo;
 import android.net.NetworkInfo;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
+import android.os.Build;
 import android.support.annotation.Nullable;
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
@@ -15,6 +16,7 @@ import android.util.Log;
 import org.apache.commons.net.util.SubnetUtils;
 import org.fdroid.fdroid.FDroidApp;
 import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.UpdateService;
 import org.fdroid.fdroid.Utils;
 import org.fdroid.fdroid.data.Repo;
 import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
@@ -42,6 +44,11 @@ import java.util.Locale;
  * changed.  Having the {@code Thread} also makes it easy to kill work
  * that is in progress.
  * <p>
+ * This also schedules an update to encourage updates happening on
+ * unmetered networks like typical WiFi rather than networks that can
+ * cost money or have caps.  The logic for checking the state of the
+ * internet connection is in {@link org.fdroid.fdroid.UpdateService#onHandleIntent(Intent)}
+ * <p>
  * Some devices send multiple copies of given events, like a Moto G often
  * sends three {@code CONNECTED} events.  So they have to be debounced to
  * keep the {@link #BROADCAST} useful.
@@ -92,6 +99,10 @@ public class WifiStateChangeService extends IntentService {
                 wifiInfoThread = new WifiInfoThread();
                 wifiInfoThread.start();
             }
+
+            if (Build.VERSION.SDK_INT < 21 && wifiState == WifiManager.WIFI_STATE_ENABLED) {
+                UpdateService.scheduleIfStillOnWifi(this);
+            }
         }
     }