Repository navigation
fix(android): keep the push alarm chain alive when jobs are held back - #2200
ArnabChatterjee20k wants to merge 2 commits into
Conversation
On Android 12+ the tick alarm hands its run to an expedited job and returns, and only that job's tick arms the next alarm. A scheduled job is accepted, not started: ColorOS held it back (thermal restriction) and never ran it, so the chain stopped until the app was opened. The alarm now arms the next alarm itself before handing off, and when its previous hand-off is still pending it runs the tick in the receiver instead of handing off again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Android 12 Robolectric test: the alarm hands off and arms the next one, and with the hand-off still pending the next alarm runs the tick in the receiver, which re-arms the chain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🔵 Tier A · Mergeable after minor fixes
The Android push alarm now arms its next tick before handing work to an expedited job. If a previous expedited job is still pending at the next alarm, the receiver cancels it and runs the tick through
Note Not approving while a bug finding is open: [minor] Don't mistake a running job for a held-back job
Fix with agent prompt### Issue 1
templates/android/library/src/main/java/io/package/services/PushBackground.kt.twig:493-494
**[minor] Don't mistake a running job for a held-back job**
`getPendingJob()` also returns jobs that have already started, and `cancel()` immediately stops a running job. Because ticks can run longer than the 15-second alarm interval (the configured wakelock allows up to 60 seconds), a slow but healthy expedited run is treated as held back; this cancels its JobService execution window and queues another tick through the receiver.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.📂 Walkthrough · 3
Reviewed |
|
@appwrite review |
|
https://github.com/appwrite review |
|
@appwrite review |
|
@appwrite review |
| if (jobs.getPendingJob(EXPEDITED_JOB_ID) != null) { | ||
| jobs.cancel(EXPEDITED_JOB_ID) |
There was a problem hiding this comment.
[minor] Don't mistake a running job for a held-back job
getPendingJob() also returns jobs that have already started, and cancel() immediately stops a running job. Because ticks can run longer than the 15-second alarm interval (the configured wakelock allows up to 60 seconds), a slow but healthy expedited run is treated as held back; this cancels its JobService execution window and queues another tick through the receiver.
Prompt To Fix With AI
This is a comment left during a code review.
Path: templates/android/library/src/main/java/io/package/services/PushBackground.kt.twig
Line: 493-494
Comment:
**[minor] Don't mistake a running job for a held-back job**
`getPendingJob()` also returns jobs that have already started, and `cancel()` immediately stops a running job. Because ticks can run longer than the 15-second alarm interval (the configured wakelock allows up to 60 seconds), a slow but healthy expedited run is treated as held back; this cancels its JobService execution window and queues another tick through the receiver.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.🟡 Minor · bug · Reply if this doesn't apply.
What does this PR do?
Fixes Android background push stopping after one run on phones that accept jobs but don't start them.
On Android 12+,
PushAlarmReceiverhands each tick to an expedited job (runExpedited) and returns. Only that job'stick()arms the next alarm.JobScheduler.schedule()returningRESULT_SUCCESSmeans the job was accepted, not that it will run. On an OPPO CPH2263 (ColorOS 13, Android 13), jobs were held back:dumpsys jobschedulershowedRestricted due to: thermal, and the expedited job sat pending with no unsatisfied constraints. So the tick never ran, the next alarm was never armed, and nothing was delivered until the app was opened again.The same thing can happen with vendor power managers, Battery Saver, or an exhausted expedited-job quota.
Changes (Android template, shared by the Android and React Native SDKs)
PushBackground.armNextAlarm()sets the next alarm only, and only while background subscriptions are active, so the chain no longer depends on the job starting. When the job does run, itstick()re-arms the samePendingIntent, which is a no-op.EXPEDITED_JOB_IDis still pending when the next alarm fires,runExpedited()cancels it and returnsfalse. The receiver then runs the tick itself through the existinggoAsync()path.Phones that run jobs normally behave as before. Phones that hold jobs back fall back to the alarm alone.
Test Plan
PushAlarmChainTest(React Native android-test,sdk = S):the alarm arms the next one itself) and passes with it.Tests,PushBackgroundUnblockTest,PushAlarmChainTest.react-native-appwrite3.2.0 on the OPPO above:TICKalarm while backgrounded, and 2 alarm wakeups in about 38 minutes.TICKalarm stays pending, with 3 more wakeups in 3 minutes.Note: the receiver-path check asserts on the re-armed job rather than the alarm, because Robolectric does not record alarms set from a background thread (the tick's worker).
Related PRs and Issues
Have you read the Contributing Guidelines on issues?
Yes.
🤖 Generated with Claude Code