We have an Android app using WorkManager to handle background sync work. Our sync worker is like this:
public class SyncWorker extends Worker {
[...]
@NonNull
@Override
public Result doWork() {
if (canNotRetry(getRunAttemptCount())) {
// This could seem unreachable, consider removing... or not... because if stopped by the
// system, the work might be retried by design
CBlogger.INSTANCE.log([...]);
return Result.success();
}
boolean syncOk = false;
//Sync
try (Realm realm = Realm.getDefaultInstance()) {
// Doing sync related ops & network calls
// checking this.isStopped() between operations to quit
// sync activity when worker has to be stopped
syncOk = true;
} catch (Throwable throwable) {
CBlogger.INSTANCE.log([...]);
}
// On error, continue with following code to avoid any logic in catch
// This method must NOT throw any unhandled exception to avoid unique work to be marked as failed
try {
if (syncOk) {
return Result.success();
}
if (canNotRetry(getRunAttemptCount() + 1)) {
CBlogger.INSTANCE.log([...]);
return Result.success();
} else {
CBlogger.INSTANCE.log([...]);
return Result.retry();
}
} catch (Throwable e) {
CBlogger.INSTANCE.log([...]);
return Result.success();
}
}
private boolean canNotRetry(int tryNumber) {
// Check if the work has been retry too many times
if (tryNumber > MAX_SYNC_RETRY_COUNT) {
CBlogger.INSTANCE.log([...]);
return true;
} else {
return false;
}
}
@Override
public void onStopped() {
CBlogger.INSTANCE.log([...]);
}
}
The work is scheduled by a dedicate method of an helper class:
public static void scheduleWorker(Context context, String syncPolicy, ExistingWorkPolicy existingWorkingPolicy){
Constraints constraints = new Constraints.Builder()
.setRequiresCharging(false)
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data.Builder data = new Data.Builder();
data.putString(context.getResources().getString(R.string.sync_worker_policy), syncPolicy);
Log.d(TAG, "Scheduling one-time sync request");
logger.info("Scheduling one-time sync request");
OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder
(SyncWorker.class)
.setInputData(data.build())
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build();
WorkManager.getInstance(context).enqueueUniqueWork("OneTimeSyncWorker", existingWorkingPolicy, oneTimeWorkRequest);
}
that is called when user clicks on "Sync" button or by another worker that is scheduled to run every 20' and calls the helper's function this way:
SyncWorkerManager.scheduleWorker(context, context.getResources().getString(R.string.sync_worker_policy_full), ExistingWorkPolicy.KEEP);
so that a new sync is queued only if not already waiting or running. Notice that sync work policy enforces that a connected network is required.
This strategy works all in all good, but sometimes we find in logs that Worker's onStopped()
method is called a few seconds (about 10") after SyncWorker start.
Known that we never programmatically stop a specific Worker for the outside and we only call WorkManager.getInstance(context).cancelAllWork();
during logout procedure or before a new login (that also schedules del periodic Worker), when does the system can decide to stop the worker and call its onStopped()
method?
I know that it can happen when:
- Constraints are no longer satisfied (network connection dropped)
- Worker runs over 10' limit imposed by JobScheduler implementation (our scenario is tested on Android 9 device)
- New unique work enqueued with same name and REPLACE policy (we never use this policy in our app for the SyncWorker, only for PeriodicSyncWorker)
- Spurious calls due to this bug (we work with "androidx.work:work-runtime:2.2.0")
Is there any other condition that can cause Worker's to be stopped? Something like:
- Doze mode
- App stand-by buckets
- App background restrictions (Settings --> Apps --> My App --> Battery --> Allow Background)
- App battery optimization (Settings --> Apps --> My App --> Battery --> Battery Optimization)
Thanks