also looking at this
fix(android): skip the whole launch intent on a relaunch from Recents
#240
2 files
+51
-9
| @@ -339,7 +339,7 @@ describe('Craft Android builder', () => { | ||
| 339 | 339 | const holder = readFileSync(join(output, 'app/src/main/java/com/craft/runtime/CraftNative.kt'), 'utf8') |
| 340 | 340 | |
| 341 | 341 | expect(activity).not.toContain('dispatch = false') |
| 342 | expect(activity).toContain('craftBridge.receiveDeepLink(launchLink)') | |
| 342 | expect(activity).toContain('intent.data?.toString()?.let(craftBridge::receiveDeepLink)') | |
| 343 | 343 | expect(activity).toContain('Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY') |
| 344 | 344 | expect(activity).toContain('intent.data?.toString()?.let(craftBridge::receiveDeepLink)') |
| 345 | 345 | // A recreated activity restores what the bridge saved, not getIntent(), |
| @@ -438,6 +438,41 @@ describe('Craft Android builder', () => { | ||
| 438 | 438 | expect(bridge).toContain('if (!window.__craftBridgeInstalled)') |
| 439 | 439 | }) |
| 440 | 440 | |
| 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 | ||
| 441 | 476 | it('routes external Activity results back to every pending media promise', async () => { |
| 442 | 477 | const output = mkdtempSync(join(tmpdir(), 'craft-android-activity-results-')) |
| 443 | 478 | await init({ name: 'WildLoop', packageName: 'org.wildloop.app', output }) |
| @@ -46,18 +46,25 @@ class MainActivity : AppCompatActivity() { | ||
| 46 | 46 | craftBridge = CraftBridge(this, webView, CraftHealthConnect(this, webView)) |
| 47 | 47 | // The link that launched the app (#215). Queued until the page's bridge |
| 48 | 48 | // is injected, then dispatched once with `initial: true`, so a page that |
| 49 | // subscribes rather than calling getInitialURL still hears it. A | |
| 50 | // relaunch from Recents carries the old intent, and was not opened by it. | |
| 51 | // A recreated activity restores what the bridge saved: its intent may | |
| 52 | // be a later warm link that onNewIntent set. | |
| 49 | // subscribes rather than calling getInitialURL still hears it. | |
| 50 | // | |
| 51 | // A relaunch from Recents carries the intent the app was first opened | |
| 52 | // with. The person picked the task out of a list, not the link or the | |
| 53 | // shortcut inside it, and neither is news the second time — so both | |
| 54 | // are skipped, not just the link (#229). | |
| 55 | // | |
| 56 | // A recreated activity restores what the bridge saved rather than | |
| 57 | // reading its intent, which onNewIntent may have replaced with a | |
| 58 | // later warm link. | |
| 59 | val launchedFromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0 | |
| 53 | 60 | if (savedInstanceState == null) { |
| 54 | val launchLink = intent.data?.toString() | |
| 55 | val launchedFromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0 | |
| 56 | if (launchLink != null && !launchedFromHistory) craftBridge.receiveDeepLink(launchLink) | |
| 61 | if (!launchedFromHistory) { | |
| 62 | intent.data?.toString()?.let(craftBridge::receiveDeepLink) | |
| 63 | handleIncomingShortcut(intent) | |
| 64 | } | |
| 57 | 65 | } else { |
| 58 | 66 | craftBridge.restoreDeepLinks(savedInstanceState) |
| 59 | 67 | } |
| 60 | if (savedInstanceState == null) handleIncomingShortcut(intent) | |
| 61 | 68 | |
| 62 | 69 | setupWebView() |
| 63 | 70 | loadContent() |