Merge branch 'update-scheduling' into 'master'

modernize update scheduling

See merge request fdroid/fdroidclient!416
This commit is contained in:
Hans-Christoph Steiner 2018-04-18 22:04:55 +00:00
commit 38a3ccda17
5 changed files with 157 additions and 55 deletions

View File

@ -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"/>

View File

@ -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
*/

View File

@ -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;
}
}

View File

@ -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);
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
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");
}
} else {
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;
}

View File

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