Skip to content

fix(android): keep the push alarm chain alive when jobs are held back - #2200

Open
ArnabChatterjee20k wants to merge 2 commits into
mainfrom
fix/push-alarm-chain-held-jobs
Open

ArnabChatterjee20k wants to merge 2 commits into
mainfrom
fix/push-alarm-chain-held-jobs

Conversation

@ArnabChatterjee20k

Copy link
Copy Markdown
Member

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+, PushAlarmReceiver hands each tick to an expedited job (runExpedited) and returns. Only that job's tick() arms the next alarm. JobScheduler.schedule() returning RESULT_SUCCESS means the job was accepted, not that it will run. On an OPPO CPH2263 (ColorOS 13, Android 13), jobs were held back: dumpsys jobscheduler showed Restricted 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)

  1. The alarm arms the next alarm itself before handing off. The new 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, its tick() re-arms the same PendingIntent, which is a no-op.
  2. No new hand-off while the previous one never started. If EXPEDITED_JOB_ID is still pending when the next alarm fires, runExpedited() cancels it and returns false. The receiver then runs the tick itself through the existing goAsync() path.

Phones that run jobs normally behave as before. Phones that hold jobs back fall back to the alarm alone.

Test Plan

  • New Robolectric test, PushAlarmChainTest (React Native android-test, sdk = S):
    • The first alarm hands off to an expedited job and arms the next alarm.
    • With that hand-off still pending, the next alarm does not hand off again, and the tick that runs in the receiver re-arms the chain.
    • Checked locally that the test fails without the fix (the alarm arms the next one itself) and passes with it.
  • Full React Native Android suite passes locally against the mock broker: Tests, PushBackgroundUnblockTest, PushAlarmChainTest.
  • On the device, with the same change patched into react-native-appwrite 3.2.0 on the OPPO above:
    • Before: no pending TICK alarm while backgrounded, and 2 alarm wakeups in about 38 minutes.
    • After: a TICK alarm 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

ArnabChatterjee20k and others added 2 commits November 6, 2026 19:49
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>
@appwrite

appwrite Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

🔵 Tier A · Mergeable after minor fixes

The pending-job check can cancel a healthy long-running expedited job, so this minor issue should be fixed before merging.

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 goAsync; a Robolectric test exercises this held-back-job path.

Verdict New comments Fixed Still open
💬 Commented 1 0 0

Note

Not approving while a bug finding is open: [minor] Don't mistake a running job for a held-back job

Finding Where
🟡 [minor] Don't mistake a running job for a held-back job templates/android/library/src/main/java/io/package/services/PushBackground.kt.twig:493
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
File Change
templates/android/library/src/main/java/io/package/services/PushBackground.kt.twig Adds detection of an existing expedited job and a helper to arm the next alarm.
templates/android/library/src/main/java/io/package/services/PushWakeups.kt.twig Arms the next alarm after a successful expedited hand-off.
tests/e2e/languages/react-native/android-test/src/test/java/Tests.kt Adds a Robolectric test for the alarm chain when an expedited job remains pending.

Reviewed 4f086ac · Details · Comment @appwrite review to re-run, or mention @appwrite with a question.

@ArnabChatterjee20k

Copy link
Copy Markdown
Member Author

@appwrite review

@ArnabChatterjee20k

Copy link
Copy Markdown
Member Author

https://github.com/appwrite review

@ArnabChatterjee20k

Copy link
Copy Markdown
Member Author

@appwrite review

@appwrite appwrite Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

@ArnabChatterjee20k

Copy link
Copy Markdown
Member Author

@appwrite review

@appwrite appwrite Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · See the inline comments. Summary

Comment on lines +493 to +494
if (jobs.getPendingJob(EXPEDITED_JOB_ID) != null) {
jobs.cancel(EXPEDITED_JOB_ID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant