fix(notifications): reliably open the tapped message from a new-mail notification
MainActivity.onCreate only parsed the incoming intent (pendingCompose /
pendingOpenMessageId) when savedInstanceState == null, on the assumption
that a non-null value always means a config-change recreation (e.g.
rotation), where Android redelivers the same, already-handled intent and
re-parsing would just navigate to a duplicate destination.
But Android also passes a restored, non-null savedInstanceState when it
recreates the activity after the process was killed in the background and
is then relaunched by tapping a notification. There, intent is the new
tap, not a replay, but the guard swallowed it exactly like a rotation, so
pendingOpenMessageId was never set and the tap silently landed wherever
the restored back stack was (typically the mailbox) instead of the
message. That's the #157 regression from the original fix in a6ec00d
(#56). pendingCompose (mailto:/share intents) went through the identical
guard and had the same latent bug.
Replace the savedInstanceState check with IntentHandledMarker, which
marks the Intent instance itself once parsed. A config-change recreation
redelivers that same marked instance, so it's correctly skipped; a
genuinely new intent -- warm via onNewIntent or cold via onCreate after a
process-death relaunch -- is never marked yet, so it's always parsed.
This dedupes on the intent's own identity instead of an unreliable proxy
for it, so it can't confuse the two recreation paths.
Adds IntentHandledMarkerTest covering the marking contract: unhandled on
first look, recognized as handled on a redelivered instance, and still
unhandled on a freshly constructed (but content-equal) instance -- the
process-death case.
Closes #157
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Locks in the #157 regression fix: [IntentHandledMarker] must tell a redelivered (already-parsed)
|
||||
* intent apart from a genuinely new one using only the [Intent] instance itself, never
|
||||
* `savedInstanceState` — which Android also sets to non-null after a process-death relaunch, where the
|
||||
* delivered intent is a brand new, not-yet-handled notification tap or mailto/share, not a replay.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class IntentHandledMarkerTest {
|
||||
|
||||
@Test
|
||||
fun first_look_marks_the_intent_and_reports_it_as_unhandled() {
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
assertTrue(IntentHandledMarker.markIfUnhandled(intent))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun redelivering_the_same_instance_is_recognized_as_already_handled() {
|
||||
// Simulates a config-change recreation: Android redelivers the very same Intent object to the
|
||||
// new Activity instance's onCreate, so a second look must be recognized as a replay.
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
IntentHandledMarker.markIfUnhandled(intent)
|
||||
assertFalse(IntentHandledMarker.markIfUnhandled(intent))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun a_freshly_constructed_equivalent_intent_is_still_unhandled() {
|
||||
// Simulates a process-death relaunch (e.g. tapping a notification after the app was killed in
|
||||
// the background): a brand new Intent instance arrives — content may equal one already marked
|
||||
// in a prior (now-dead) process, but it was never marked in THIS one, so it must be parsed.
|
||||
val first = Intent(Intent.ACTION_VIEW)
|
||||
IntentHandledMarker.markIfUnhandled(first)
|
||||
|
||||
val second = Intent(Intent.ACTION_VIEW)
|
||||
assertTrue(IntentHandledMarker.markIfUnhandled(second))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user