ReviewOS

also looking at this

craft-native/craft

fix(android): skip the whole launch intent on a relaunch from Recents

#240
Merged glennmichael123 wants to merge fix/android-recents-relaunch into main
2 files +51 -9
packages/android/src/index.test.tsmodified+36-1
Changes to packages/android/src/index.test.ts
@@ -339,7 +339,7 @@ describe('Craft Android builder', () => {
339339 const holder = readFileSync(join(output, 'app/src/main/java/com/craft/runtime/CraftNative.kt'), 'utf8')
340340
341341 expect(activity).not.toContain('dispatch = false')
342 expect(activity).toContain('craftBridge.receiveDeepLink(launchLink)')
342 expect(activity).toContain('intent.data?.toString()?.let(craftBridge::receiveDeepLink)')
343343 expect(activity).toContain('Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY')
344344 expect(activity).toContain('intent.data?.toString()?.let(craftBridge::receiveDeepLink)')
345345 // A recreated activity restores what the bridge saved, not getIntent(),
@@ -438,6 +438,41 @@ describe('Craft Android builder', () => {
438438 expect(bridge).toContain('if (!window.__craftBridgeInstalled)')
439439 })
440440
441 it('ignores the launch intent on a relaunch from Recents, and the saved one on a recreate', async () => {
442 // #229, two halves. The shortcut path had no history check, so the
443 // shortcut the app was first opened with was dispatched again every time
444 // the task was reopened from Recents. And a recreated activity must read
445 // what the bridge saved, not getIntent(), which onNewIntent replaces with
446 // the latest warm link — that half landed with #232 and is pinned here so
447 // it cannot quietly go back.
448 const output = mkdtempSync(join(tmpdir(), 'craft-android-recents-'))
449 await init({ name: 'WildLoop', packageName: 'org.wildloop.app', output, config: { enableDeepLinks: true, urlSchemes: ['wildloop'] } })
450
451 const activity = readFileSync(join(output, 'app/src/main/java/org/wildloop/app/MainActivity.kt'), 'utf8')
452 const onCreate = activity.slice(activity.indexOf('override fun onCreate'), activity.indexOf('private fun setupWebView'))
453
454 // One flag, guarding both, rather than the link alone.
455 expect(onCreate).toContain('Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY')
456 expect(onCreate.match(/launchedFromHistory/g)?.length).toBe(2)
457 // Bounded by the block's own closing brace: a slice that ran to the end
458 // of onCreate would still find a shortcut call that had moved back out.
459 const guardStart = onCreate.indexOf('if (!launchedFromHistory) {')
460 const guard = onCreate.slice(guardStart, onCreate.indexOf('\n }', guardStart))
461 expect(guard).toContain('craftBridge::receiveDeepLink')
462 expect(guard).toContain('handleIncomingShortcut(intent)')
463 // And exactly once, so it cannot also be dispatched outside the guard.
464 expect(onCreate.match(/handleIncomingShortcut\(intent\)/g)?.length).toBe(1)
465
466 // A genuinely new intent still carries both through.
467 const onNewIntent = activity.slice(activity.indexOf('override fun onNewIntent'), activity.indexOf('override fun onBackPressed'))
468 expect(onNewIntent).toContain('craftBridge::receiveDeepLink')
469 expect(onNewIntent).toContain('handleIncomingShortcut(intent)')
470
471 // And a recreate reads the bundle, never the intent.
472 expect(onCreate).toContain('craftBridge.restoreDeepLinks(savedInstanceState)')
473 expect(activity).toContain('craftBridge.saveDeepLinks(outState)')
474 })
475
441476 it('routes external Activity results back to every pending media promise', async () => {
442477 const output = mkdtempSync(join(tmpdir(), 'craft-android-activity-results-'))
443478 await init({ name: 'WildLoop', packageName: 'org.wildloop.app', output })