Bug - regression from prior changes - notification now does not load directly to the email message it displays again #157

Closed
opened 2026-07-02 20:24:03 +00:00 by JMR-dev · 0 comments
JMR-dev commented 2026-07-02 20:24:03 +00:00 (Migrated from github.com)

Context

Tapping a new-mail notification is supposed to open that specific message's reader directly
(fixed in a6ec00d, "fix(notifications): open the tapped message from a new-mail notification",
#56). This ticket reports that behavior regressing.

The mechanism: MailNotifier.openMessageIntent() builds a PendingIntent targeting
MainActivity with a per-message data URI (NotificationIntents.openMessage). MainActivity
is meant to pick the id back up in either onCreate or onNewIntent and hand it to LibreMailApp
as pendingOpenMessageId, which a LaunchedEffect turns into navController.navigate(Routes.reader(messageId)).

A concrete gap found while tracing this path: in MainActivity.onCreate
(MainActivity.kt, ~lines 80-83), the id is only parsed when savedInstanceState == null:

// Only on a fresh launch — on a config-change recreation the NavHost restores the compose /
// reader destination itself, so re-parsing the (unchanged) intent would open a duplicate.
if (savedInstanceState == null) {
    pendingCompose.value = IntentComposeParser.parse(intent)
    pendingOpenMessageId.value = NotificationIntents.messageId(intent)
}

This guard was written for configuration-change recreation (e.g. rotation), where the
Activity is destroyed and recreated with the same, already-handled intent — skipping re-parse
there correctly avoids re-navigating to a duplicate destination. But Android also passes a
non-null savedInstanceState when it recreates the Activity after the process was killed
(common in the background, e.g. after the app has been backgrounded for a while) and the user
then taps the notification to relaunch it: in that case onCreate receives both a restored
savedInstanceState (from before the kill) and the new tapped-notification Intent — but the
current guard treats this exactly like the rotation case and never parses the new intent, so
pendingOpenMessageId is never set and the tap silently lands on whatever destination NavHost's
saved state restores (typically the mailbox), not the message. onNewIntent (used when the
process is still alive and MainActivity is reused via FLAG_ACTIVITY_SINGLE_TOP/
FLAG_ACTIVITY_CLEAR_TOP) has no such guard and looks correct — so a warm-process tap should
still work, which fits a report of intermittent/"used to work" regression rather than total
breakage.

This is the leading, code-confirmed hypothesis, but hasn't been confirmed as the regression on
a device yet — see Scope. It's also worth checking interaction with the app-lock gate
(AppLockGateHost, ui/lock/AppLockGateHost.kt), since several app-lock commits landed between
the original notification fix and now and that code sits directly between MainActivity and
LibreMailApp in the composition tree (content is only composed after hasEverUnlocked, which
is a plain remember — i.e. always false again after a process death).

Scope

  • Reproduce on a device across the matrix that matters here: process alive vs. process killed
    before the tap (adb shell am kill <package> or waiting out background eviction), and
    app-lock on vs. off — confirm which combination(s) actually fail.
  • Fix the savedInstanceState == null guard in MainActivity.onCreate so a process-death
    relaunch (new intent + restored instance state) still parses the incoming intent, while a
    genuine config-change recreation (same intent, already handled) still does not re-navigate.
    One approach: check whether the current intent differs from what's already been
    consumed, rather than branching solely on savedInstanceState.
  • pendingCompose (mailto:/share intents) goes through the identical guard and is likely
    affected by the same class of bug — worth covering in the same fix even though this ticket
    is scoped to notifications.
  • Add a regression test (Robolectric/instrumented) simulating an Activity recreation with a
    non-null savedInstanceState and a fresh OPEN_MESSAGE intent, asserting the reader still
    opens.

Acceptance criteria

  • Tapping a new-mail notification opens directly to that message's reader in all of: warm process
    (app already running/backgrounded) and cold process (killed, relaunched via the tap), with
    app-lock both on and off.
  • Rotation / other config-change recreation still doesn't re-navigate or duplicate the back stack
    (no regression on the case the original guard was protecting).

Relevant files

  • MainActivity.kt (~lines 63-107), ui/LibreMailApp.kt (~lines 85-92),
    notifications/NotificationIntents.kt, notifications/MailNotifier.kt,
    ui/lock/AppLockGateHost.kt.

Dependencies

None. Related history: a6ec00d (original fix, #56).

## Context Tapping a new-mail notification is supposed to open that specific message's reader directly (fixed in a6ec00d, "fix(notifications): open the tapped message from a new-mail notification", #56). This ticket reports that behavior regressing. The mechanism: `MailNotifier.openMessageIntent()` builds a `PendingIntent` targeting `MainActivity` with a per-message data URI (`NotificationIntents.openMessage`). `MainActivity` is meant to pick the id back up in either `onCreate` or `onNewIntent` and hand it to `LibreMailApp` as `pendingOpenMessageId`, which a `LaunchedEffect` turns into `navController.navigate(Routes.reader(messageId))`. **A concrete gap found while tracing this path:** in `MainActivity.onCreate` (`MainActivity.kt`, ~lines 80-83), the id is only parsed when `savedInstanceState == null`: ```kotlin // Only on a fresh launch — on a config-change recreation the NavHost restores the compose / // reader destination itself, so re-parsing the (unchanged) intent would open a duplicate. if (savedInstanceState == null) { pendingCompose.value = IntentComposeParser.parse(intent) pendingOpenMessageId.value = NotificationIntents.messageId(intent) } ``` This guard was written for **configuration-change recreation** (e.g. rotation), where the Activity is destroyed and recreated with the *same*, already-handled intent — skipping re-parse there correctly avoids re-navigating to a duplicate destination. But Android also passes a non-null `savedInstanceState` when it recreates the Activity **after the process was killed** (common in the background, e.g. after the app has been backgrounded for a while) and the user then taps the notification to relaunch it: in that case `onCreate` receives both a restored `savedInstanceState` (from before the kill) *and* the new tapped-notification `Intent` — but the current guard treats this exactly like the rotation case and **never parses the new intent**, so `pendingOpenMessageId` is never set and the tap silently lands on whatever destination NavHost's saved state restores (typically the mailbox), not the message. `onNewIntent` (used when the process is still alive and MainActivity is reused via `FLAG_ACTIVITY_SINGLE_TOP`/ `FLAG_ACTIVITY_CLEAR_TOP`) has no such guard and looks correct — so a warm-process tap should still work, which fits a report of intermittent/"used to work" regression rather than total breakage. This is the leading, code-confirmed hypothesis, but hasn't been confirmed as *the* regression on a device yet — see Scope. It's also worth checking interaction with the app-lock gate (`AppLockGateHost`, `ui/lock/AppLockGateHost.kt`), since several app-lock commits landed between the original notification fix and now and that code sits directly between `MainActivity` and `LibreMailApp` in the composition tree (content is only composed after `hasEverUnlocked`, which is a plain `remember` — i.e. always false again after a process death). ## Scope - [ ] Reproduce on a device across the matrix that matters here: process alive vs. process killed before the tap (`adb shell am kill <package>` or waiting out background eviction), and app-lock on vs. off — confirm which combination(s) actually fail. - [ ] Fix the `savedInstanceState == null` guard in `MainActivity.onCreate` so a process-death relaunch (new intent + restored instance state) still parses the incoming intent, while a genuine config-change recreation (same intent, already handled) still does not re-navigate. One approach: check whether the *current* `intent` differs from what's already been consumed, rather than branching solely on `savedInstanceState`. - [ ] `pendingCompose` (mailto:/share intents) goes through the identical guard and is likely affected by the same class of bug — worth covering in the same fix even though this ticket is scoped to notifications. - [ ] Add a regression test (Robolectric/instrumented) simulating an Activity recreation with a non-null `savedInstanceState` and a fresh `OPEN_MESSAGE` intent, asserting the reader still opens. ## Acceptance criteria - Tapping a new-mail notification opens directly to that message's reader in all of: warm process (app already running/backgrounded) and cold process (killed, relaunched via the tap), with app-lock both on and off. - Rotation / other config-change recreation still doesn't re-navigate or duplicate the back stack (no regression on the case the original guard was protecting). ## Relevant files - `MainActivity.kt` (~lines 63-107), `ui/LibreMailApp.kt` (~lines 85-92), `notifications/NotificationIntents.kt`, `notifications/MailNotifier.kt`, `ui/lock/AppLockGateHost.kt`. ## Dependencies None. Related history: a6ec00d (original fix, #56).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JMR-dev/LibreMail#157