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 currentintent 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).
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).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 aPendingIntenttargetingMainActivitywith a per-message data URI (NotificationIntents.openMessage).MainActivityis meant to pick the id back up in either
onCreateoronNewIntentand hand it toLibreMailAppas
pendingOpenMessageId, which aLaunchedEffectturns intonavController.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 whensavedInstanceState == null: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
savedInstanceStatewhen 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
onCreatereceives both a restoredsavedInstanceState(from before the kill) and the new tapped-notificationIntent— but thecurrent guard treats this exactly like the rotation case and never parses the new intent, so
pendingOpenMessageIdis never set and the tap silently lands on whatever destination NavHost'ssaved state restores (typically the mailbox), not the message.
onNewIntent(used when theprocess 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 shouldstill 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 betweenthe original notification fix and now and that code sits directly between
MainActivityandLibreMailAppin the composition tree (content is only composed afterhasEverUnlocked, whichis a plain
remember— i.e. always false again after a process death).Scope
before the tap (
adb shell am kill <package>or waiting out background eviction), andapp-lock on vs. off — confirm which combination(s) actually fail.
savedInstanceState == nullguard inMainActivity.onCreateso a process-deathrelaunch (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
intentdiffers from what's already beenconsumed, rather than branching solely on
savedInstanceState.pendingCompose(mailto:/share intents) goes through the identical guard and is likelyaffected by the same class of bug — worth covering in the same fix even though this ticket
is scoped to notifications.
non-null
savedInstanceStateand a freshOPEN_MESSAGEintent, asserting the reader stillopens.
Acceptance criteria
(app already running/backgrounded) and cold process (killed, relaunched via the tap), with
app-lock both on and off.
(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).