Status Update
Comments
ra...@google.com <ra...@google.com> #2
This is an inherently unsafe thing to ask for (which is why we have no built in support for this use case), but to explain why, I'll need to go into a lot of detail with what 'running suspend code when a lifecycle state is at least X' implies.
Namely: What behavior do you want to have if the Lifecycle falls below your chosen Lifecycle.State
while your suspending work is running?
The way the when
APIs worked is that the work was paused - when you hit a suspension point, your code just wouldn't run anymore. This is purposefully not supported anymore (as it could leave things hanging for a very long time if for instance the user hit the Home button).
So the only other options are (ignoring the case when the Lifecycle is DESTROYED
and the whole coroutine scope is cancelled):
1. Let the suspending code continue to run to completion.
This has the benefit that the entire block runs atomically - i.e., it wouldn't be cancelled half way through.
That approach looks like:
lifecycleScope.launch {
// Suspend until you are STARTED
withStarted { }
// Run your code that happens after you become STARTED here
doYourOneTimeWork()
// Note: your code will continue to run even if the Lifecycle falls below STARTED
}
This type of code comes with the assumption that your one time work does not depend on staying above your state, but that's not something a library could know. For example, if you run a FragmentTransaction
after your suspending work, the state might be saved prior to that call happening.
2. Cancel the suspending code and restart it when you come back above that state.
This is exactly the contract for repeatOnLifecycle
- re-running the block every time you go back above the given State. However, there's no way for a library to know if it is safe for you to re-run the entire block or if you need to checkpoint more often.
In the naive case, where you can run the entire block multiple times until it actually completes successfully, this is just tracking a isComplete
flag:
lifecycleScope.launch {
var isComplete = false
repeatOnLifecycle(Lifecycle.State.STARTED) {
if (!isComplete) {
// Do your work here. It will be canceled if the Lifecycle
// goes down while it is running
doYourOneTimeWork()
// Then mark the work as complete
isComplete = true
}
}
}
Of course, the 'it is safe to rerun the entire block if it gets canceled half way through' is a really big assumption.
3. Cancel the suspending code and don't restart it
This is similar to the previous one, but uses a finally
block to set the isComplete
flag no matter if the work was cancelled or not.
lifecycleScope.launch {
var isComplete = false
repeatOnLifecycle(Lifecycle.State.STARTED) {
if (!isComplete) {
try {
// Do your work here. It will be canceled if the Lifecycle
// goes down while it is running
doYourOneTimeWork()
} finally {
// By marking the work as complete in the finally block,
// we never restart the block just leaving it potentially
// half complete.
isComplete = true
}
}
}
}
I'm struggling to find a valid use case where you want to leave the work only partially finished, so I mostly include this for completeness.
I hope this gives a little more background on this problem space and why only we have the APIs we have - ideally, you'd avoid all of these cases entirely by not trying to run one-time suspending code when a lifecycle state is at least X.
st...@deliveroo.co.uk <st...@deliveroo.co.uk> #3
Thanks for the detailed response! I think my majority use cases fall into either 1 or 3, i.e. if user initiated an action and then closed the activity/fragment, I would like the action (as a suspend block) to also get cancelled upon lifecycle reaching destroyed. I guess in fact for use case 3, I really just need lifecycleScope.launch
without even needing repeatOnLifecycle
. (In fact, it sounds like launchWhenCreated
acts identically as launch
...)
da...@google.com <da...@google.com> #4
In examples 2 and 3 wouldn't repeatOnLifecycle
continue to be executed every time the lifecycle state gets to STARTED
, even after isComplete
has been set to true
? Is there no way to tell the repeatOnLifecycle function that it can stop repeating?
ra...@google.com <ra...@google.com> #5
Re launch
to cancel the whole code block if you want. Obviously that only works well if you are doing a naive all or nothing approach and not something where you are check-pointing at multiple points in your suspending block.
st...@deliveroo.co.uk <st...@deliveroo.co.uk> #6
st...@deliveroo.co.uk <st...@deliveroo.co.uk> #7
va...@gmail.com <va...@gmail.com> #8
da...@google.com <da...@google.com> #9
Cause it might get confusing for people finding that documentation page and then being told that it's gonna be deprecated.
ap...@google.com <ap...@google.com> #10
Branch: androidx-main
commit 4616a739983e6316819dc515057677edf40a41c1
Author: Daniel Santiago Rivera <danysantiago@google.com>
Date: Wed Jul 24 08:59:34 2024
Catch SecurityException in startForeground
In API 34, startForeground() will throw SecurityException if the service type prerequisites are not met. WorkManager won't be able to start the foreground service but the worker is likely to continue and ideally fail once it checks the permission needed to do the actual work. The worker will also be subject to JobScheduler limitations.
This change mitigates the crashing issue of a foreground worker running and the app losing the prerequisite permission, causing the app to restart and due to SystemForegroundService being sticky, it restarting too but failing at startForeground() due to missing permissions.
Bug: 333957914
Test: Manual
Change-Id: I96607f62fdd456290163ee6f2774f9d31c64e79e
M work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java
ap...@google.com <ap...@google.com> #11
Branch: androidx-main
commit c540fa0564df85fa4e3cfd2d98f532f4120e12d8
Author: Daniel Santiago Rivera <danysantiago@google.com>
Date: Wed Jul 24 09:48:38 2024
Add foreground worker with location
Updates WorkManager integration app to validate Android 14 foreground service type requirements, including the addition for a foreground worker who uses location APIs.
Bug: 333957914
Test: n/a
Change-Id: I9ac76d029ab0f0e4d1256145bdc2e3cb20b27c36
M work/integration-tests/testapp/lint-baseline.xml
M work/integration-tests/testapp/src/main/AndroidManifest.xml
A work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundLocationWorker.kt
M work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
M work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.kt
M work/integration-tests/testapp/src/main/res/layout/activity_main.xml
M work/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
rw...@gmail.com <rw...@gmail.com> #12
<[ https://android-review.googlesource.com/3190158 :// = > https://android-X-review.android-X-source. com/3190158 and I didn't get my addresses I need <[ Android-X-Developer.com ]> <[Android-X-GloblaleWaybandTelcomPrivateGovSecurityServices.com ] [ Android-X-GlobaleEcosystemEnterprise ]
:// I need everything of this to be Android-X Object script because they are trying to say it's their's and they can't even run it and I need it back ]>
Description
Version used: 2.9.0
Devices/Android versions reproduced on: API 34
When we target API 34, WorkManager is crashing our application on startup if the "location" permissions are removed whilst the application is running.
Everything works fine whilst the application has the "location" permissions. When the "location" permissions are removed (whilst the application is running), the OS is killing the application (OS behaviour) and because SystemForegroundService is a STICKY service, the OS tries to restart the service which causes a security exception because the "location" permissions are no longer present.
This is a realistic user scenario which will happen many times in our production app. We can not prevent our application from crashing as this relates to the STICKY SystemForegroundService. Our application, can check the "location" permissions before scheduling the "worker", but we have no control over the user removing the "location" permissions after the "worker" has been scheduled by the WorkManager.
This behaviour only happens with WorkManager when targeting API 34 as per the restrictions documented here:
Here is the crash we are seeing on application startup:
Java.lang.SecurityException: Starting FGS with type location callerApp=ProcessRecord{34bbf76 32120:com.deliveroo.driverapp.test/u0a190} targetSDK=34 requires permissions: all of the permissions allOf=true [android.permission.FOREGROUND_SERVICE_LOCATION] any of the permissions allOf=false [android.permission.ACCESS_COARSE_LOCATION, android.permission.ACCESS_FINE_LOCATION] and the app must be in the eligible state/exemptions to access the foreground only permission
We are using the WorkManager to run a CoroutineWorker which accesses the device location. We are scheduling the Worker as follows:
val request = OneTimeWorkRequest.Builder(worker.java)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
ExistingWorkPolicy.KEEP,
request,
)
We are specifying FOREGROUND_SERVICE_TYPE_LOCATION in the ForegroundInfo:
ForegroundInfo(ONLINE_NOTIFICATION_ID, notificationsManager.onlineNotification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION)
Our Android manifest specifies the "location" foregroundServiceType:
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="location"/>
As stated above, everything works fine until the user removes the "location" permissions after the "worker" has started. Then the OS kills our application and the OS tries to recover the STICKY SystemForegroundService which crashes because the app no longer as "location" permissions.
Please can you advise how we can avoid WorkManager crashing our application in the above scenario.