Summary
On Android, a notification scheduled through the batch command is written
to NotificationStorage with the correct id but a payload serialized as
the literal string null. Everything that later reads that store fails:
LocalNotificationRestoreReceiver runs on boot, finds the saved ids,
deserializes nothing, and restores no alarms — scheduled
notifications silently disappear across a reboot.
getPending() rejects.
cancelAll() rejects.
cancel([id]) is unaffected (it takes ids as arguments and never reads the
stored payload), which is a useful workaround but not a fix.
Environment
tauri-plugin-notification 2.3.3, @tauri-apps/plugin-notification 2.3.3
- Tauri 2,
targetSdk 36, minSdk 28
- Reproduced on two devices, both Android 16 / API 36:
- emulator
sdk_gphone64_arm64 (AOSP)
- OnePlus 15 (CPH2749), OxygenOS
CPH2749_16.0.9.400
Steps to reproduce
-
Schedule a notification through the Android batch command with an
interval schedule, e.g.
await invoke("plugin:notification|batch", {
notifications: [
{
id: 7039373,
channelId: "reminders",
title: "Test",
body: "Test",
schedule: Schedule.interval({ hour: 20, minute: 0, second: 0 }, true),
},
],
});
(Invoked directly because guest-js 2.3.3 exposes no batch wrapper and
the Rust plugin registers only notify / request_permission /
is_permission_granted — see "Additional notes".)
-
Confirm the alarm was registered:
adb shell dumpsys alarm | grep -A3 TimedNotificationPublisher
The alarm is scheduled correctly, so the scheduling half works.
-
Inspect the plugin's own store:
adb shell run-as <app-id> cat shared_prefs/NOTIFICATION_STORE.xml
Observed:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="7039373">null</string>
</map>
The id is stored; the notification is not.
-
Call pending() → rejects. Call cancelAll() → rejects.
-
Reboot the device without launching the app, then check:
adb shell dumpsys alarm | grep -c TimedNotificationPublisher
Zero alarms are registered, and the scheduled notification never fires.
Logcat confirms the receiver itself did run:
ActivityManager: Start proc <pid>:<app-id> for broadcast
{<app-id>/app.tauri.notification.LocalNotificationRestoreReceiver}
So the boot path is wired correctly — it simply has nothing usable to
restore.
Expected
- The stored payload round-trips, so the boot receiver can re-register the
schedule and the notification fires after a reboot.
getPending() returns the pending notifications.
cancelAll() cancels them.
Actual
The stored payload is the string null; boot restore is a no-op and both
store-reading commands reject.
Impact
Any app using the plugin for reminders loses all scheduled notifications
on reboot, with no error at schedule time and nothing in the logs to explain
it — the failure is invisible until a user notices a reminder never arrived.
Notes on scope (what we did not verify)
- We captured that
getPending()/cancelAll() reject, but not the exact
exception text (our test harness stringified the error poorly). The
connection to the null payload is inference from the stored file plus
the code path, not from a captured stack trace.
sendNotification() with a schedule never persists at all — it goes
through the window.Notification polyfill → notify → show, which does
not call NotificationStorage. That may be intended, but the JS API does
expose schedule on sendNotification, so a caller can reasonably expect
a scheduled notification to survive a reboot. Worth clarifying in either
code or docs.
Secondary report (same issue or its own — maintainers' call)
The exact→inexact fallback is silent
setExactIfPossible checks canScheduleExactAlarms() and, when it returns
false, falls back to setAndAllowWhileIdle/set — with no log line and no
signal to the caller. An app that asked for an exact alarm gets an inexact
one and cannot tell.
This matters more than it sounds: on a device where the fallback triggers, a
reminder scheduled for 20:00 can be delivered minutes to hours late, and
there is nothing in the app's logs, the plugin's logs, or the JS return value
to indicate a downgrade happened. Diagnosing it requires reading
dumpsys alarm and knowing what window= means.
A single Log.d on the fallback branch — or better, surfacing the
canScheduleExactAlarms() result through the JS API so an app can warn the
user — would turn an invisible failure into a diagnosable one.
Measured contrast, same APK and same granted USE_EXACT_ALARM:
| device |
scheduled alarm |
| AOSP emulator, API 36 |
window=0 … flags=0x5 (exact) |
| OnePlus 15, OxygenOS, API 36 |
window=+9m20s … flags=0x4 (inexact) |
Possibly related
Summary
On Android, a notification scheduled through the
batchcommand is writtento
NotificationStoragewith the correct id but a payload serialized asthe literal string
null. Everything that later reads that store fails:LocalNotificationRestoreReceiverruns on boot, finds the saved ids,deserializes nothing, and restores no alarms — scheduled
notifications silently disappear across a reboot.
getPending()rejects.cancelAll()rejects.cancel([id])is unaffected (it takes ids as arguments and never reads thestored payload), which is a useful workaround but not a fix.
Environment
tauri-plugin-notification2.3.3,@tauri-apps/plugin-notification2.3.3targetSdk36,minSdk28sdk_gphone64_arm64(AOSP)CPH2749_16.0.9.400Steps to reproduce
Schedule a notification through the Android
batchcommand with aninterval schedule, e.g.
(Invoked directly because guest-js 2.3.3 exposes no
batchwrapper andthe Rust plugin registers only
notify/request_permission/is_permission_granted— see "Additional notes".)Confirm the alarm was registered:
The alarm is scheduled correctly, so the scheduling half works.
Inspect the plugin's own store:
Observed:
The id is stored; the notification is not.
Call
pending()→ rejects. CallcancelAll()→ rejects.Reboot the device without launching the app, then check:
Zero alarms are registered, and the scheduled notification never fires.
Logcat confirms the receiver itself did run:
So the boot path is wired correctly — it simply has nothing usable to
restore.
Expected
schedule and the notification fires after a reboot.
getPending()returns the pending notifications.cancelAll()cancels them.Actual
The stored payload is the string
null; boot restore is a no-op and bothstore-reading commands reject.
Impact
Any app using the plugin for reminders loses all scheduled notifications
on reboot, with no error at schedule time and nothing in the logs to explain
it — the failure is invisible until a user notices a reminder never arrived.
Notes on scope (what we did not verify)
getPending()/cancelAll()reject, but not the exactexception text (our test harness stringified the error poorly). The
connection to the
nullpayload is inference from the stored file plusthe code path, not from a captured stack trace.
sendNotification()with aschedulenever persists at all — it goesthrough the
window.Notificationpolyfill →notify→show, which doesnot call
NotificationStorage. That may be intended, but the JS API doesexpose
scheduleonsendNotification, so a caller can reasonably expecta scheduled notification to survive a reboot. Worth clarifying in either
code or docs.
Secondary report (same issue or its own — maintainers' call)
The exact→inexact fallback is silent
setExactIfPossiblecheckscanScheduleExactAlarms()and, when it returnsfalse, falls back to
setAndAllowWhileIdle/set— with no log line and nosignal to the caller. An app that asked for an exact alarm gets an inexact
one and cannot tell.
This matters more than it sounds: on a device where the fallback triggers, a
reminder scheduled for 20:00 can be delivered minutes to hours late, and
there is nothing in the app's logs, the plugin's logs, or the JS return value
to indicate a downgrade happened. Diagnosing it requires reading
dumpsys alarmand knowing whatwindow=means.A single
Log.don the fallback branch — or better, surfacing thecanScheduleExactAlarms()result through the JS API so an app can warn theuser — would turn an invisible failure into a diagnosable one.
Measured contrast, same APK and same granted
USE_EXACT_ALARM:window=0 … flags=0x5(exact)window=+9m20s … flags=0x4(inexact)Possibly related
active()returns an empty object on Android instead of list of active notifications #1897 (active()returns an empty object on Android) — that reporter also callspending(); if theirpending()was rejecting too, it may share this root cause.active(),pending(),cancel()throwCommand not foundon macOS #1898 (active()/pending()/cancel()throw "Command not found" on macOS) — separate issue, but consistent with what we saw: the desktop plugin registers onlynotify,request_permissionandis_permission_granted.