Reputation: 359
I have a long-running worker that performs network operations. If a network error happens during the operations, the worker is stopped with the Retry
worker result. Android then automatically restarts the worker after a given delay if network conditions are met.
The worker is enqueued as follow:
val workRequest = PeriodicWorkRequestBuilder<MyWorker>(
repeatInterval = 1L,
repeatIntervalTimeUnit = TimeUnit.HOURS,
)
.setConstraints(NetworkType.CONNECTED)
.addBackoffCriteria(30L)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
MyWorker.workerUniqueName,
ExistingPeriodicWorkPolicy.KEEP,
workRequest,
)
And setForegroundAsync()
is called before running the long-time operation inside the doWork()
of the worker.
I also follow the guidelines described in the Android guide - Support for long-running workers. The foreground service type declared is dataSync
.
Given the worker is started with 4G+Wifi, When the Wifi is disabled, Then the worker is stopped (with the above Retry
worker result). In that case, every time the system tries to restart the worker (retry policy) while the app is in background, I have the following exception in an Android 14 device:
Moving WorkSpec (...) to the foreground
startForegroundService() not allowed due to mAllowStartForeground false: service com.myapp/androidx.work.impl.foreground.SystemForegroundService
android.app.ForegroundServiceStartNotAllowedException: startForegroundService() not allowed due to mAllowStartForeground false: service com.myapp/androidx.work.impl.foreground.SystemForegroundService
at android.app.ForegroundServiceStartNotAllowedException$1.createFromParcel(ForegroundServiceStartNotAllowedException.java:54)
at android.app.ForegroundServiceStartNotAllowedException$1.createFromParcel(ForegroundServiceStartNotAllowedException.java:50)
at android.os.Parcel.readParcelableInternal(Parcel.java:4870)
at android.os.Parcel.readParcelable(Parcel.java:4852)
at android.os.Parcel.createExceptionOrNull(Parcel.java:3052)
at android.os.Parcel.createException(Parcel.java:3041)
at android.os.Parcel.readException(Parcel.java:3024)
at android.os.Parcel.readException(Parcel.java:2966)
at android.app.IActivityManager$Stub$Proxy.startService(IActivityManager.java:5984)
at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1931)
at android.app.ContextImpl.startForegroundService(ContextImpl.java:1906)
at android.content.ContextWrapper.startForegroundService(ContextWrapper.java:830)
at androidx.core.content.ContextCompat$Api26Impl.startForegroundService(ContextCompat.java:1189)
at androidx.core.content.ContextCompat.startForegroundService(ContextCompat.java:752)
at androidx.work.impl.Processor.startForeground(Processor.java:206)
at androidx.work.impl.utils.WorkForegroundUpdater$1.run(WorkForegroundUpdater.java:100)
at androidx.work.impl.utils.SerialExecutorImpl$Task.run(SerialExecutorImpl.java:96)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
...
Moving WorkSpec (...) to the foreground
Work [ id=..., tags={ ...MyWorker } ] failed because it threw an exception/error
java.util.concurrent.ExecutionException: android.app.BackgroundServiceStartNotAllowedException: Not allowed to start service Intent { act=ACTION_NOTIFY cmp=com.myapp/androidx.work.impl.foreground.SystemForegroundService (has extras) }: app is in background uid UidRecord{... TRNB bg:+2m17s737ms idle change:procadj procs:0 seq(401644,401374)} caps=------
at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:515)
at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:474)
at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:316)
at androidx.work.impl.utils.SerialExecutorImpl$Task.run(SerialExecutorImpl.java:96)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
...
Worker result FAILURE for Work [ id=..., tags={ ...MyWorker } ]
Unable to stop foreground service
android.app.BackgroundServiceStartNotAllowedException: Not allowed to start service Intent { act=ACTION_STOP_FOREGROUND cmp=com.myapp/androidx.work.impl.foreground.SystemForegroundService }: app is in background uid UidRecord{... TRNB bg:+2m17s772ms idle change:procadj procs:0 seq(401644,401374)} caps=------
at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1945)
at android.app.ContextImpl.startService(ContextImpl.java:1900)
at android.content.ContextWrapper.startService(ContextWrapper.java:825)
at androidx.work.impl.Processor.stopForegroundService(Processor.java:403)
at androidx.work.impl.Processor.cleanUpWorkerUnsafe(Processor.java:425)
at androidx.work.impl.Processor.onExecuted(Processor.java:345)
at androidx.work.impl.Processor.lambda$startWork$1$androidx-work-impl-Processor(Processor.java:179)
at androidx.work.impl.Processor$$ExternalSyntheticLambda2.run(D8$$SyntheticClass:0)
...
In other cases I tested where the worker is stopped and relaunched while app was in background, I didn't get the above exception.
Do you have any idea why I have this exception, in that case and only in that case? Have you faced similar issues?
Upvotes: 5
Views: 223
Reputation: 45
I ran into the same problem and and this is how I got around the problem. There might be a better way, but this worked for me.
First off, the problem you and I ran into might be an Android bug. From your stacktrace the line Processor.cleanUpWorkerUnsafe(Processor.java:425)
is from the following method
private WorkerWrapper cleanUpWorkerUnsafe(@NonNull String id) {
WorkerWrapper wrapper = mForegroundWorkMap.remove(id);
boolean wasForeground = wrapper != null;
if (!wasForeground) {
wrapper = mEnqueuedWorkMap.remove(id);
}
mWorkRuns.remove(id);
if (wasForeground) {
stopForegroundService();
}
return wrapper;
}
In that method it checks if the worker task wasForeground
. If that was false, then this exception would not happen.
Or maybe the bug is actually in the stopForegroundSerice()
call:
private void stopForegroundService() {
synchronized (mLock) {
boolean hasForegroundWork = !mForegroundWorkMap.isEmpty();
if (!hasForegroundWork) {
Intent intent = createStopForegroundIntent(mAppContext);
try {
// Wrapping this inside a try..catch, because there are bugs the platform
// that cause an IllegalStateException when an intent is dispatched to stop
// the foreground service that is running.
mAppContext.startService(intent);
} catch (Throwable throwable) {
Logger.get().error(TAG, "Unable to stop foreground service", throwable);
}
// Release wake lock if there is no more pending work.
if (mForegroundLock != null) {
mForegroundLock.release();
mForegroundLock = null;
}
}
}
}
Anyway, enough speculating. On to the workaround that worked for me.
I noticed in the documentation for setForegroundAsync that it stated:
Calling setForegroundAsync will fail with an IllegalStateException when the process is subject to foreground service restrictions. Consider using androidx. work. WorkRequest. Builder. setExpedited(OutOfQuotaPolicy) and getForegroundInfoAsync() instead.
This solution worked for me. I update my builder for the work request with the setExpedited
call:
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(NsUploaderWorker.class)
.addTag(NsUploaderWorker.WORKER_TAG)
.setInputData(inputData)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build();
WorkManager.getInstance(context).enqueue(uploadWorkRequest);
And then instead of using setForegroundAsync
to display the notification, I manually added it using notificationManager.notify(NOTIFICATION_ID, notification);
, and in I wrapped everything in the doWork() method with a try/catch/finally, and put notificationManager.cancel(NOTIFICATION_ID);
in the finally block. Here is a shortened version of my doWork() method:
@NonNull
@Override
public Result doWork() {
try {
Notification notification = notificationHelper.createNotification(notificationManager);
notificationManager.notify(NOTIFICATION_ID, notification);
Timber.d("Starting upload process...");
// Do something here...
return Result.success(getResultData(uploadResultBundle));
} catch (Exception e) {
UploadResultBundle uploadResultBundle = new UploadResultBundle();
uploadResultBundle.markAllFailure();
return Result.failure(getResultData(uploadResultBundle));
} finally {
notificationManager.cancel(NOTIFICATION_ID);
}
}
Upvotes: 0