tauri-apps / tauri-apps/plugins-workspace

[notification] Android: scheduled notifications are persisted as the string "null", breaking boot restore, getPending() and cancelAll()

Open
#3,550 1 comment 0 reactions 0 assignees View on GitHub
platform: android plugin: notification type: bug
Dominant language
Rust
Stars
1.8k
Forks
602
Avg merge
4d 14h
Merged PRs (30d)
9

Description

### 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:

1. `LocalNotificationRestoreReceiver` runs on boot, finds the saved ids,
deserializes nothing, and restores **no alarms** — scheduled
notifications silently disappear across a reboot.
2. `getPending()` rejects.
3. `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

1. Schedule a notification through the Android `batch` command with an
interval schedule, e.g.

```ts
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".)

2. Confirm the alarm was registered:

```
adb shell dumpsys alarm | grep -A3 TimedNotificationPublisher
```

The alarm is scheduled correctly, so the scheduling half works.

3. Inspect the plugin's own store:

```
adb shell run-as cat shared_prefs/NOTIFICATION_STORE.xml
```

Observed:

```xml


null

```

The id is stored; the notification is not.

4. Call `pending()` → rejects. Call `cancelAll()` → rejects.

5. 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 : for broadcast
{/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

- #1897 (`active()` returns an empty object on Android) — that reporter also calls `pending()`; if their `pending()` was rejecting too, it may share this root cause.
- #1898 (`active()` / `pending()` / `cancel()` throw "Command not found" on macOS) — separate issue, but consistent with what we saw: the desktop plugin registers only `notify`, `request_permission` and `is_permission_granted`.

Contributor guide

Open the contributing guide

Research direction

Start by tracing the Android batch command into NotificationStorage and LocalNotificationRestoreReceiver, then inspect how the notification payload becomes the string "null" in NOTIFICATION_STORE.xml. Reproduce the schedule, reboot, getPending(), and cancelAll() behavior, and verify that stored notifications round-trip and restore after boot. Treat the setExactIfPossible fallback as a separate scope unless maintainers combine it.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, rust
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.