From 5f3e952958a1a9a3df2bcdd32c6fbd754ae86e07 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 10 Nov 2016 16:04:46 +0100 Subject: [PATCH 1/4] move "Update Interval" pref handling to Preferences class Basically all of the settings are handled in the Preferences class now, this was an outlier. --- app/src/main/java/org/fdroid/fdroid/Preferences.java | 11 +++++++++++ .../main/java/org/fdroid/fdroid/UpdateService.java | 11 +++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index 228e9f960..f17c237a0 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,16 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh */ private static final String PREF_CACHE_APK = "cacheDownloaded"; + public int getUpdateInterval() { + try { + String value = preferences.getString(PREF_UPD_INTERVAL, + String.valueOf(DEFAULT_UPD_INTERVAL)); + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return DEFAULT_UPD_INTERVAL; + } + } + /** * 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/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 20360a5f8..96f6038f9 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -121,11 +121,7 @@ public class UpdateService extends IntentService { * is changed, or c) on startup, in case we get upgraded. */ public static void schedule(Context ctx) { - - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(ctx); - String sint = prefs.getString(Preferences.PREF_UPD_INTERVAL, "0"); - int interval = Integer.parseInt(sint); + int interval = Preferences.get().getUpdateInterval(); Intent intent = new Intent(ctx, UpdateService.class); PendingIntent pending = PendingIntent.getService(ctx, 0, intent, 0); @@ -290,13 +286,12 @@ public class UpdateService extends IntentService { * @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); + int interval = Preferences.get().getUpdateInterval(); if (interval == 0) { Log.i(TAG, "Skipping update - disabled"); return false; } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); long lastUpdate = prefs.getLong(STATE_LAST_UPDATED, 0); long elapsed = System.currentTimeMillis() - lastUpdate; if (elapsed < interval * 60 * 60 * 1000) { From 7d9f5e880cd63521db4f37c8a352570795bad43c Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 10 Nov 2016 20:40:23 +0100 Subject: [PATCH 2/4] move update scheduling entirely to AlarmManager This changes the flow of the update triggering so that any Intent sent to UpdateService can potentially trigger an update, depending only on the state of the internet and the "Only on WiFi" preference. Instead of having a timer that checks every hour to see if it is time to run the update, just let AlarmManager send a trigger Intent based on the timing in the "Update Interval" setting. The update schedule is reset each time F-Droid restarts, and also each time the user returns from the settings, so if AlarmManager fails us in the time being, the updates will be rescheduled next time F-Droid is restarted, the device is rebooted, etc. refs #662 --- .../java/org/fdroid/fdroid/Preferences.java | 9 ++++- .../java/org/fdroid/fdroid/UpdateService.java | 39 ++----------------- 2 files changed, 11 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index f17c237a0..4dc49628e 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -158,14 +158,19 @@ 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)); - return Integer.parseInt(value); + hours = Integer.parseInt(value); } catch (NumberFormatException e) { - return DEFAULT_UPD_INTERVAL; + hours = DEFAULT_UPD_INTERVAL; } + return hours * 60 * 60 * 1000; } /** diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 96f6038f9..2a8ee528c 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -131,8 +131,7 @@ public class UpdateService extends IntentService { alarm.cancel(pending); if (interval > 0) { alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, - SystemClock.elapsedRealtime() + 5000, - AlarmManager.INTERVAL_HOUR, pending); + SystemClock.elapsedRealtime() + 5000, interval, pending); Utils.debugLog(TAG, "Update scheduler alarm set"); } else { Utils.debugLog(TAG, "Update scheduler alarm not set"); @@ -275,34 +274,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() { - int interval = Preferences.get().getUpdateInterval(); - if (interval == 0) { - Log.i(TAG, "Skipping update - disabled"); - return false; - } - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); - 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. */ @@ -334,6 +305,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)) { @@ -344,12 +316,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; } From e36d7719b3a5fce24a6b1ec4a4a10ccf10bd8565 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 14 Nov 2016 12:23:37 +0100 Subject: [PATCH 3/4] trigger an update after joining good wifi without JobSchedule Each time the device connects to a wifi network, this waits for 2 minutes, then if the wifi is still connected, it re-schedules the index update to happen now. The goal is to favor unmetered networks as much as possible when downloading the index and any automatic app updates. This is only needed on older platforms, JobScheduler handles this for us on android-21+ --- .../java/org/fdroid/fdroid/UpdateService.java | 47 +++++++++++++++++++ .../fdroid/net/WifiStateChangeService.java | 11 +++++ 2 files changed, 58 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 2a8ee528c..b30cbc13d 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -27,6 +27,7 @@ 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 +52,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; @@ -147,6 +149,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 { + + private final WeakReference 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(); 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. *

+ * 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)} + *

* 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); + } } } From 9e0de9ac69102519bda1bdefe757897af122e21a Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 18 Apr 2018 22:34:13 +0200 Subject: [PATCH 4/4] rudimentary support for JobScheduler to run updates The new JobScheduler API can opportunitistically run a job based on whether there is good internet, connected to power, etc. This is very useful for running updates. Ideally, updates would always happen in the background while on unmetered internet and connected to power. #588 --- app/src/main/AndroidManifest.xml | 4 ++ .../org/fdroid/fdroid/UpdateJobService.java | 43 +++++++++++++++++ .../java/org/fdroid/fdroid/UpdateService.java | 47 +++++++++++++------ 3 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/UpdateJobService.java 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 @@ + 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 Project Volta: Scheduling jobs + */ +@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 b30cbc13d..73f7591cd 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -22,7 +22,10 @@ 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; @@ -118,27 +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 Project Volta: Scheduling jobs */ - public static void schedule(Context ctx) { + public static void schedule(Context context) { int interval = Preferences.get().getUpdateInterval(); - Intent intent = new Intent(ctx, UpdateService.class); - PendingIntent pending = PendingIntent.getService(ctx, 0, intent, 0); + if (Build.VERSION.SDK_INT < 21) { + Intent intent = new Intent(context, UpdateService.class); + PendingIntent pending = PendingIntent.getService(context, 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, interval, 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); } - } /**