Skip to content

fix(push): report restricted background usage as best effort - #2014

Merged
ArnabChatterjee20k merged 3 commits into
mainfrom
fix/push-background-restricted
Oct 13, 2026
Merged

ArnabChatterjee20k merged 3 commits into
mainfrom
fix/push-background-restricted

Conversation

@abnegate

Copy link
Copy Markdown
Member

Summary

When the user sets an Android app's battery usage to Restricted, Android may hold its alarms, jobs and foreground service while it is closed, so pushes wait until the app is opened. backgroundStatus() didn't see this and reported bestEffort=false. It now reports backgroundRestricted (native Android, React Native bridge, Flutter channel), and bestEffort accounts for it.

@ArnabChatterjee20k could you review?

Root cause

backgroundStatus() only looked at exact alarms, battery-optimisation exemption and foreground mode. It never checked ActivityManager.isBackgroundRestricted() (the RUN_ANY_IN_BACKGROUND app-op).

Repro (device testing)

Found while running the Android device scenarios, plus new edge cases, on Android emulators (native Android and React Native SDKs) against a real Appwrite main instance:

  1. adb shell appops set <pkg> RUN_ANY_IN_BACKGROUND ignore (Settings > Apps > app > Battery > Restricted), and remove the battery exemption.
  2. Close the app and send pushes: nothing arrives until the app is opened.
  3. backgroundStatus() reported exact=true battery=false bestEffort=false on both SDKs.

Fix

  • PushBackgroundStatus.backgroundRestricted is isBackgroundRestricted() on Android 9+ and false below. It is appended last with a false default, and the constructor is @JvmOverloads for Java callers.
  • The bridge JSON key is "backgroundRestricted". It is also in the React Native PushBackgroundStatus type and the Flutter model (fromMap treats a missing key as false).
  • bestEffort is true when the app is restricted and not battery-exempt, and foreground mode stops counting while the app is restricted:
    • AOSP AppStateTrackerImpl runs alarms and jobs of power-allowlisted apps before checking the app-op, and requestIgnoreBatteryOptimizations allowlists the app without resetting the op, so the exemption lifts the hold on wake-ups.
    • AOSP ActiveServices stops a restricted app's foreground service once it leaves the screen, exempt or not.
    • So a restricted, exempt app's bestEffort follows exact alarms alone.
  • The READMEs and API docs (Android, React Native, Flutter) tell apps to ask the user to choose Unrestricted in the app's battery settings, or to request the exemption.

Tests

  • Android Tests.kt: new Push background restricted line (the lanes run at API 23, so it checks the guard), and PushBackgroundRestrictedTest at sdk = P covering restricted, exempt and foreground-mode cases against an unrestricted baseline. Fails on main (compile) and with a getter that ignores the exemption or foreground mode.
  • React Native android-test: PushBridgeBackgroundStatusTest checks the bridge JSON key and bestEffort. Fails on main.
  • Flutter: new test/src/push_background_status_test.dart (parsing and defaults). Fails on main.
  • React Native push.node.js also asserts the field; that line is coverage only, since the mock passes the JSON through.

Notes

  • The React Native TS field is required (the native code ships in the same package); Kotlin and Dart default it to false. Worth a changelog line.
  • The local Docker lanes were limited by host resources: Android16Java17 and ReactNativeAndroid passed before the last bestEffort refinement; host Robolectric and Flutter cover the final head. CI is the gate.
  • Not yet confirmed on a device: Restricted → requestIgnoreBatteryOptimizations → Allow → close the app → push (expect delivery through the scheduled wake-ups).
  • Follow-up: optionally a helper that opens the app's battery settings.

🤖 Generated with Claude Code

abnegate and others added 2 commits October 13, 2026 22:45
With background usage set to Restricted in the app's battery settings
(RUN_ANY_IN_BACKGROUND ignore), Android may hold the app's alarms, jobs
and foreground service while it is closed, so messages can wait until
the app is opened. backgroundStatus() only looked at exact alarms and
foreground mode, so it still reported bestEffort=false and apps had no
signal to ask the user to lift it.

PushBackgroundStatus gains backgroundRestricted, the raw
ActivityManager.isBackgroundRestricted() on Android 9+ and false below,
and the React Native and Flutter bridges carry it as
"backgroundRestricted". bestEffort is true when it is set and the app is
not exempt from battery optimisation: AppStateTracker lets alarms and
jobs of power-allowlisted apps run before it checks the app-op, and the
exemption request allowlists the app without resetting the op, so
isBackgroundRestricted() stays true while the scheduled wake-ups work
again. Foreground mode doesn't count while it is set, exemption or not:
ActiveServices only lets a restricted app hold a foreground service
while it is on screen, stops the service once the app leaves it, and
refuses startForeground() from the background, so a restricted, exempt
app's bestEffort follows exact alarms alone. The README and KDoc tell
apps to ask for Unrestricted, or for the exemption, since neither exact
alarms nor foreground mode lift it.

The field is appended last with a false default, and the constructor is
@jvmoverloads so Java callers of the three-argument constructor keep
compiling; Kotlin callers keep compiling through the default.

The Robolectric lanes run Tests.kt at API 23, where only the guard is
reachable, so the printed check covers that. PushBackgroundRestrictedTest
at API 28 drives ShadowActivityManager and ShadowPowerManager to cover
the restricted and exempt cases, and checks that foreground mode doesn't
lift the restriction, and PushBridgeBackgroundStatusTest in the React
Native Android harness checks the same through the bridge's JSON, since
PushBridge is only generated into React Native and Flutter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a user sets an app's Android battery usage to Restricted, Android
may hold its alarms, jobs and foreground service while it is closed, so
messages can wait until the app is opened. backgroundStatus() still
reported bestEffort=false on such devices, giving apps no way to notice
or explain it.

The native status now carries backgroundRestricted and folds it into
bestEffort unless the app is power-allowlisted, and stops counting
foreground mode while it is set, since Android stops a restricted app's
foreground service once it leaves the screen, exemption or not. Surface
the field in the React Native PushBackgroundStatus type and the Flutter
model so apps can read it, and document the two ways out: the user
choosing Unrestricted in the app's battery settings, or granting the
battery optimisation exemption, after which bestEffort reflects only the
exact-alarm state. Neither exact alarms nor foreground mode lift it.

The Dart field defaults to false so callers of the public const
constructor keep compiling, and fromMap treats a missing key as false.
A Flutter unit test covers the parsed value and both defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hansi-codes

hansi-codes Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

🟢 Tier S · Ready to merge

This change adds Android background-restriction reporting to the push status API and includes it in the bestEffort calculation. It exposes the field through the Android bridge, React Native and Flutter, and updates the SDK guidance and tests.

Verdict New comments Fixed Still open
✅ Approved 0 0 0
📂 Walkthrough · 15
File Change
src/SDK/Language/Flutter.php Registers the Flutter background-status test template.
templates/android/README.md.twig Documents restricted background usage and its effect on delivery.
templates/android/library/src/main/java/io/package/services/Push.kt.twig Updates the Android API documentation for the new status field.
templates/android/library/src/main/java/io/package/services/PushBackground.kt.twig Reads Android's background-restricted state on API 28 and later.
templates/android/library/src/main/java/io/package/services/PushBridge.kt.twig Adds backgroundRestricted to the bridge status JSON.
templates/android/library/src/main/java/io/package/services/PushCore.kt.twig Adds the status field and accounts for restriction and battery exemption in bestEffort.
templates/flutter/README.md.twig Documents restricted background usage and its effect on delivery.
templates/flutter/lib/src/mqtt.dart.twig Adds the field to the Flutter model and parses it with a false default.
templates/flutter/test/src/push_background_status_test.dart.twig Tests Flutter parsing and default behavior for the new field.
templates/react-native/README.md.twig Documents restricted background usage and demonstrates handling it.
templates/react-native/src/services/push.ts.twig Adds the field to the React Native status type and API documentation.
tests/e2e/Base.php Adds the restricted-background scenario to Android expected responses.
tests/e2e/languages/android/Tests.kt Tests status reporting and bestEffort for restricted usage.
tests/e2e/languages/react-native/android-test/Tests.kt Tests restriction reporting through the React Native Android bridge.
tests/e2e/languages/react-native/push.node.js Asserts that the React Native status includes the new field.

Merged main (512edea); no changes to this PR's code since b25c935 · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes 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 ArnabChatterjee20k left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed. backgroundRestricted comes from isBackgroundRestricted() on Android 9+. Restricted without the battery exemption reports best effort; with the exemption it follows exact alarms, and foreground mode no longer counts. Unrestricted is unchanged. It stays backward compatible (a false default, @JvmOverloads, the Flutter parser defaulting a missing key). The React Native field being required is worth a changelog line.

@ArnabChatterjee20k
ArnabChatterjee20k merged commit 3ac568b into main Oct 13, 2026
61 checks passed
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.

2 participants