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