Commit e6ef957
fix(bindings): promote MeshForegroundService to foreground in onCreate + add Stop action (#278)
* fix(bindings): promote MeshForegroundService to foreground in onCreate
Android gives an app 5 seconds after startForegroundService() to reach
startForeground(). MeshForegroundService currently calls startForeground()
from onStartCommand, which on cold start and on resume can be delayed past
that window by JS-thread initialisation and main-thread work. When that
happens the OS terminates the process with a fatal RemoteServiceException.
Move the startForeground() call into onCreate so the deadline is
unreachable regardless of what runs on the main thread after service
creation. The subsequent startForeground() calls in onStartCommand are
idempotent on the same service instance and are safe re-promotes.
Also add a "Stop" notification action so the user can shut mesh down from
the notification shade. It routes back through the service's own
ACTION_STOP handler via PendingIntent.getForegroundService on Android O+
(getService on older releases), so no separate BroadcastReceiver is
required and the delivery is legal under the background service-start
restrictions that apply when the user taps the action while the app is in
the background.
Observed impact: this fix has been shipping in-app via a patch-package
override since v67 of the MINE app; the underlying crash was the top
ANR-adjacent fatal on Redmi Note 8 Pro (Android 11), fingerprinted by
Sentry as REACT-NATIVE-5Z.
Retires the MINE-side patch tracked by Linear OFF-1801.
* fix(bindings): make the mesh notification Stop action stop the mesh
The Stop action added alongside the onCreate promotion routes
straight back into the service's own ACTION_STOP handler, which
drops the keep-alive and nothing else.
But this service is *only* a keep-alive — the module owns the
protocol and the transports, exactly as the class comment has
said all along. So tapping Stop cleared the notification and the
foreground protection while BLE, WiFi Direct, Nostr and the
process scheduler kept right on running, with nothing told to
JS. The user sees "mesh off" while the radios keep draining the
battery until the OS gets around to reaping the process.
Give the notification its own action that hands off to a host
callback instead. The module runs the same teardown as its
JS-facing stop() and emits mesh_stopped_by_user so app state
can't silently diverge, and the service stays up until that
teardown comes back around through ACTION_STOP — clearing the
notification while the mesh is still running is exactly the lie
we're fixing here. With no host registered we still drop the
keep-alive, because a dead button is its own kind of bug.
While at it, the try/catch only wrapped the onCreate promotion;
the other two call sites were bare. That matters, because a
connectedDevice promotion throws once the Nearby-Devices
permissions are revoked — so the guarded failure came straight
back uncaught a few milliseconds later. All three share a helper
now. It is not immunity: if the instance came from
startForegroundService() and promotion genuinely fails, the
system still raises its own timeout kill, and the helper says so.
And stop() is a no-op when nothing is running, since creating an
instance purely to tear it down now means a notification flash on
a path the app already runs twice.
The new event tag is emitted by the bridge, not by the core enum,
so it joins the bridge-only allowlist in the types.ts drift
guard. That guard is doing exactly what it was built for — it
caught the omission before I did.
The tests pin the part that is invisible in the source: the
service is in the foreground by the end of onCreate, with no
start command delivered. Move that promotion back into
onStartCommand and the code still reads fine while the
five-second deadline is quietly reachable again.
* fix(bindings): serialize mesh teardown and always run its tail
The notification Stop action added earlier in this branch runs the
same teardown as the JS-facing stop(), except it does it on its own
thread. Nothing was stopping the two from overlapping, and a user
tapping Stop while the app foregrounds and calls stop() is not an
exotic scenario.
Two threads interleaving through stopTransportsAndProtocol
double-stop every transport mid-pass. Worse, if both throw — and BLE
teardown throwing on real devices is the entire reason #279 and #280
exist — neither pass ever reaches the remaining transports, the
keep-alive service, or the protocol core. The user-stop path then
clears the notification and tells JS the mesh is down while five
transports are still burning battery.
That is precisely the lie this branch was written to prevent.
The JS path had the same hole without needing a race at all: one
throwing transport skipped the foreground-service stop and
protocol.stop() outright, so the notification kept advertising an
active mesh over a half-dead stack.
So serialize the shared teardown, and move the keep-alive and core
shutdown into a finally. The second entrant through the lock re-runs
the stops after a completed pass, which is their idempotent no-op
path. The exception still propagates, so stop() rejects exactly as
it did before.
While at it, drop the now-redundant keep-alive stop from the
user-stop path — the shared function guarantees it now.
* test(bindings): stop leaking the start-request flag between tests
The service tracks start intent in a companion field, and onDestroy
is what clears it. A test that calls start() without ever creating
an instance therefore leaves that flag set for whatever runs next —
there is no service instance for the fixture to destroy.
Nothing is red today, because the one test that cares re-establishes
its own precondition. That is luck, not design, and it lasts exactly
until someone adds a test between those two.
So just call stop() in the fixture. It no-ops once both flags are
already down, which costs nothing on every other test.
---------
Co-authored-by: bahdotsh <appu.yess@gmail.com>1 parent 137715b commit e6ef957
6 files changed
Lines changed: 536 additions & 40 deletions
File tree
- bindings/react-native
- android
- src
- main/java/com/offlineprotocol
- test/java/com/offlineprotocol
- src
- crates/offline-protocol/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
89 | 95 | | |
90 | 96 | | |
91 | 97 | | |
| |||
Lines changed: 152 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
27 | | - | |
| 28 | + | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
37 | 39 | | |
38 | 40 | | |
39 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
40 | 51 | | |
41 | 52 | | |
42 | 53 | | |
43 | 54 | | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
44 | 64 | | |
45 | 65 | | |
46 | 66 | | |
| |||
49 | 69 | | |
50 | 70 | | |
51 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
52 | 88 | | |
| 89 | + | |
53 | 90 | | |
54 | 91 | | |
55 | 92 | | |
| |||
61 | 98 | | |
62 | 99 | | |
63 | 100 | | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
64 | 113 | | |
65 | 114 | | |
66 | 115 | | |
| |||
79 | 128 | | |
80 | 129 | | |
81 | 130 | | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
82 | 142 | | |
83 | 143 | | |
84 | 144 | | |
85 | 145 | | |
86 | 146 | | |
87 | 147 | | |
88 | | - | |
89 | | - | |
90 | | - | |
| 148 | + | |
91 | 149 | | |
92 | 150 | | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
93 | 155 | | |
94 | 156 | | |
95 | | - | |
96 | | - | |
| 157 | + | |
97 | 158 | | |
98 | 159 | | |
99 | 160 | | |
100 | 161 | | |
101 | 162 | | |
102 | | - | |
103 | | - | |
| 163 | + | |
104 | 164 | | |
105 | 165 | | |
106 | 166 | | |
| |||
109 | 169 | | |
110 | 170 | | |
111 | 171 | | |
| 172 | + | |
112 | 173 | | |
113 | 174 | | |
114 | 175 | | |
115 | 176 | | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
116 | 235 | | |
117 | 236 | | |
118 | 237 | | |
| |||
129 | 248 | | |
130 | 249 | | |
131 | 250 | | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
132 | 275 | | |
133 | 276 | | |
134 | 277 | | |
135 | 278 | | |
136 | 279 | | |
137 | 280 | | |
138 | 281 | | |
| 282 | + | |
139 | 283 | | |
140 | 284 | | |
141 | 285 | | |
0 commit comments