Repository navigation
fix(push): report restricted background usage as best effort - #2014
Conversation
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>
🟢 Tier S · Ready to mergeThis change adds Android background-restriction reporting to the push status API and includes it in the
📂 Walkthrough · 15
Merged |
ArnabChatterjee20k
left a comment
There was a problem hiding this comment.
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.
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 reportedbestEffort=false. It now reportsbackgroundRestricted(native Android, React Native bridge, Flutter channel), andbestEffortaccounts for it.@ArnabChatterjee20k could you review?
Root cause
backgroundStatus()only looked at exact alarms, battery-optimisation exemption and foreground mode. It never checkedActivityManager.isBackgroundRestricted()(theRUN_ANY_IN_BACKGROUNDapp-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
maininstance:adb shell appops set <pkg> RUN_ANY_IN_BACKGROUND ignore(Settings > Apps > app > Battery > Restricted), and remove the battery exemption.backgroundStatus()reportedexact=true battery=false bestEffort=falseon both SDKs.Fix
PushBackgroundStatus.backgroundRestrictedisisBackgroundRestricted()on Android 9+ and false below. It is appended last with a false default, and the constructor is@JvmOverloadsfor Java callers."backgroundRestricted". It is also in the React NativePushBackgroundStatustype and the Flutter model (fromMaptreats a missing key as false).bestEffortis true when the app is restricted and not battery-exempt, and foreground mode stops counting while the app is restricted:AppStateTrackerImplruns alarms and jobs of power-allowlisted apps before checking the app-op, andrequestIgnoreBatteryOptimizationsallowlists the app without resetting the op, so the exemption lifts the hold on wake-ups.ActiveServicesstops a restricted app's foreground service once it leaves the screen, exempt or not.bestEffortfollows exact alarms alone.Tests
Tests.kt: newPush background restrictedline (the lanes run at API 23, so it checks the guard), andPushBackgroundRestrictedTestatsdk = Pcovering 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.PushBridgeBackgroundStatusTestchecks the bridge JSON key andbestEffort. Fails on main.test/src/push_background_status_test.dart(parsing and defaults). Fails on main.push.node.jsalso asserts the field; that line is coverage only, since the mock passes the JSON through.Notes
bestEffortrefinement; host Robolectric and Flutter cover the final head. CI is the gate.requestIgnoreBatteryOptimizations→ Allow → close the app → push (expect delivery through the scheduled wake-ups).🤖 Generated with Claude Code