@@ -53,7 +53,15 @@ class MainActivity : FragmentActivity() {
53 private var openFileHandled = false
53 private var openFileHandled = false
54 // Set when a deep link was present but failed validation (malformed id/path). Surfaced
54 // Set when a deep link was present but failed validation (malformed id/path). Surfaced
55 // as a toast from onResume so the user knows the link was rejected, not silently ignored.
55 // as a toast from onResume so the user knows the link was rejected, not silently ignored.
56+ // On a warm start (onNewIntent while already resumed) the toast is shown immediately from
57+ // handleIntent, since onResume is NOT re-called on an already-resumed activity — without
58+ // that the toast would stay pending until the next pause/resume cycle (e.g. user
59+ // backgrounds and returns), making the rejected link look silently ignored.
56 private var showInvalidLinkToast = false
60 private var showInvalidLinkToast = false
61+ // True between onResume and onPause. Used by handleIntent to decide whether to fire the
62+ // invalid-link toast immediately (warm start) or defer it to onResume (cold start, where
63+ // a toast from onCreate could flash before the first frame is drawn).
64+ private var isResumed = false
57 // Cold-start prompts (crash report, notification rationale) rendered as Compose M3
65 // Cold-start prompts (crash report, notification rationale) rendered as Compose M3
58 // dialogs. Hoisted as Activity-level mutableState so the non-composable trigger logic
66 // dialogs. Hoisted as Activity-level mutableState so the non-composable trigger logic
59 // (onCreate/onResume) can flip them and the Compose tree (inside OpencodeTheme) reads
67 // (onCreate/onResume) can flip them and the Compose tree (inside OpencodeTheme) reads
@@ -244,19 +252,34 @@ class MainActivity : FragmentActivity() {
245 override fun onResume() {
253 override fun onResume() {
246 super.onResume()
254 super.onResume()
247 // Re-check on resume: an Activity recreated by a config change not in configChanges
256 // Re-check on resume: an Activity recreated by a config change not in configChanges
248 // (e.g. locale) has savedInstanceState != null, so onCreate skipped the request. This
257 // (e.g. locale) has savedInstanceState != null, so onCreate skipped the request. This
249 // ensures the user is prompted once per Activity instance when still ungranted rather
258 // ensures the user is prompted once per Activity instance when still ungranted rather
250 // than silently losing all notifications.
259 // than silently losing all notifications.
251 maybeRequestNotificationPermission()
260 maybeRequestNotificationPermission()
252 // Surface a rejected deep link once, so a malformed opencode:// link doesn't appear
261 // Surface a rejected deep link once, so a malformed opencode:// link doesn't appear
253− // to be silently ignored. Cleared after firing so a rotation doesn't re-toast.
262+ // to be silently ignored. Cleared after firing so a rotation doesn't re-toast. This
263+ // covers the cold-start path (handleIntent ran from onCreate, before the activity was
264+ // resumed, so it deferred the toast here). Warm-start rejections fire immediately from
265+ // handleIntent and never set this flag.
254 if (showInvalidLinkToast) {
266 if (showInvalidLinkToast) {
255 showInvalidLinkToast = false
267 showInvalidLinkToast = false
256− android.widget.Toast.makeText(this, R.string.deep_link_invalid, android.widget.Toast.LENGTH_SHORT).show()
268+ showInvalidLinkToastNow()
272+ override fun onPause() {
277+ /** Fire the invalid-deep-link toast. Extracted so the cold-start (onResume) and warm-start
278+ * (handleIntent) paths share a single implementation. */
279+ private fun showInvalidLinkToastNow() {
280+ android.widget.Toast.makeText(this, R.string.deep_link_invalid, android.widget.Toast.LENGTH_SHORT).show()
260 /** Capture text shared from another app so it can be prefilled into a session draft,
283 /** Capture text shared from another app so it can be prefilled into a session draft,
261 * and session ids from notification taps / deep links so we can open them. */
284 * and session ids from notification taps / deep links so we can open them. */
262 private fun handleIntent(intent: Intent?) {
285 private fun handleIntent(intent: Intent?) {
@@ -333,7 +356,11 @@ class MainActivity : FragmentActivity() {
333 // The link carried a session host but no valid id (or a blank/traversal
356 // The link carried a session host but no valid id (or a blank/traversal
334 // attempt). Surface a toast so the user knows the link was rejected,
357 // attempt). Surface a toast so the user knows the link was rejected,
335 // rather than silently no-op'ing (which reads as "the app did nothing").
358 // rather than silently no-op'ing (which reads as "the app did nothing").
359+ // On a warm start (already resumed) fire immediately; on a cold start
360+ // defer to onResume so the toast doesn't race the first frame.
336− if (!seg.isNullOrBlank()) showInvalidLinkToast = true
361+ if (!seg.isNullOrBlank()) {
362+ if (isResumed) showInvalidLinkToastNow() else showInvalidLinkToast = true
337 // Mark the link handled even on rejection, so a process-death-and-restore
364 // Mark the link handled even on rejection, so a process-death-and-restore
338 // (which re-delivers the same intent) doesn't re-fire the toast on every
365 // (which re-delivers the same intent) doesn't re-fire the toast on every
339 // restore — the rejection is final, not a transient state to retry.
366 // restore — the rejection is final, not a transient state to retry.
@@ -348,7 +375,10 @@ class MainActivity : FragmentActivity() {
348 if (!openFileHandled) {
375 if (!openFileHandled) {
349 rawPath?.takeIf { isValidFilePath(it) }
376 rawPath?.takeIf { isValidFilePath(it) }
350 ?.let { container.requestOpenFile(it); openFileHandled = true } ?: run {
377 ?.let { container.requestOpenFile(it); openFileHandled = true } ?: run {
351− if (!rawPath.isNullOrBlank()) showInvalidLinkToast = true
378+ if (!rawPath.isNullOrBlank()) {
379+ // Same warm-vs-cold logic as the session branch above.
380+ if (isResumed) showInvalidLinkToastNow() else showInvalidLinkToast = true
352 // Mark handled on rejection too, mirroring the session branch —
382 // Mark handled on rejection too, mirroring the session branch —
353 // otherwise a process-death restore re-fires the invalid-link toast.
383 // otherwise a process-death restore re-fires the invalid-link toast.
354 openFileHandled = true
384 openFileHandled = true