From 57d1f93a7a708e60921c7c536662f5f8f296dbe0 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Wed, 26 Aug 2026 18:44:13 -0700 Subject: [PATCH] feat: 9.1 NativeWindow updates --- content/guide/multi-window.md | 451 ++++++++++++++++++++++------------ 1 file changed, 291 insertions(+), 160 deletions(-) diff --git a/content/guide/multi-window.md b/content/guide/multi-window.md index 6bf543f7..01c01526 100644 --- a/content/guide/multi-window.md +++ b/content/guide/multi-window.md @@ -1,31 +1,52 @@ --- title: Multiple Windows -description: Develop with multiple windows on supported devices. +description: Develop with multiple windows on iOS and Android. contributors: - NathanWalker + - edusperoni --- -NativeScript 9 adds first-class support for iOS multi-window (multi-scene) applications by adopting the UIScene lifecycle when enabled. This guide explains how to enable scenes, how NativeScript integrates with them, and how to work with scene-specific APIs and events. +NativeScript 9.1 introduces `NativeWindow`, a cross-platform window API. A single JavaScript context can drive several windows: UIScene-backed windows on iOS (iPadOS Split View and Stage Manager, visionOS) and activity-backed windows on Android (split screen, desktop windowing, foldables). :::tip Why this matters -Apple is moving all iOS apps to the UIScene lifecycle. Enabling scenes now makes your app future‑proof and unlocks multiple windows on iPadOS and visionOS. +Apple is moving all iOS apps to the UIScene lifecycle and will require it in an upcoming release. Adopting scenes now makes your app future-proof and unlocks multiple windows on iPadOS and visionOS. +::: + +## Concepts + +There are two objects to keep apart: + +- **Application** — the process and the JavaScript context. There is exactly one, no matter how many windows are open. It is the browser. +- **NativeWindow** — one OS window: a `UIWindowScene` plus its `UIWindow` on iOS, an `Activity` on Android. It is a tab. + +Every app registers at least one window — the **primary** window — including apps that never open a second one. Each window carries: + +| Property | Description | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | Stable identifier for the lifetime of the JavaScript context. It survives a detach and re-attach, so it correlates a window across an iOS scene reconnect or an Android activity recreation. | +| `role` | `application`, `embedded`, `carplay` or `externalDisplay`. | +| `state` | `attached` (connected to a live native surface), `detached` (the surface went away but the window can be reconnected) or `closed`. | +| `isPrimary` | At most one window is primary at a time. When the primary window closes, another attached window is promoted. | +| `rootView` | The view tree this window hosts. | + +:::tip Two ways to structure a multi-window app +Windows share one JavaScript context, so you decide how much else they share. Either treat every window as a fully separate app — bootstrap it with its own services and state — or bootstrap once and give each window another view tree over shared state, closer to modal navigation. ::: ## Supported platforms -- iPad running iPadOS 13 or later (multi-window capable) -- visionOS (Vision Pro) -- iPhone: runs with UIScene lifecycle; multiple windows are not exposed to users, but adopting UIScene is recommended +- **iPadOS 13+** — Split View, Slide Over and Stage Manager show windows side by side. +- **visionOS** — multiple windows. +- **iPhone** — runs the UIScene lifecycle, but the system exposes a single window. +- **Android** — `Application.openWindow()` is experimental. Each window is an activity in its own task: on a phone the new window covers the current one and both appear in recents, while split screen, desktop windowing or a foldable shows them side by side. -## Prerequisites +The per-window APIs and events on this page work everywhere, including single-window apps. -- NativeScript 9+ -- iOS 13+ runtime -- Xcode/iOS tooling capable of building with UIScene (Xcode 11+) +## Enabling multiple windows -## Enable scene lifecycle (Info.plist) +### iOS: add the scene manifest -NativeScript will automatically switch to UIScene lifecycle when a scene manifest is present in your iOS app `Info.plist`. Add the following keys: +NativeScript switches to the UIScene lifecycle when a scene manifest is present in `App_Resources/iOS/Info.plist`: ```xml UIApplicationSceneManifest @@ -34,213 +55,323 @@ NativeScript will automatically switch to UIScene lifecycle when a scene manifes UIWindowSceneSessionRoleApplication UIApplicationSupportsMultipleScenes - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - SceneDelegate - - - ``` -When this configuration is detected, NativeScript adopts UIScene; on devices that don’t support scenes, your app continues to behave as a single-window app. +That is the whole manifest. NativeScript installs its scene configuration on your application delegate at runtime, so `UISceneConfigurations` and `UISceneDelegateClassName` no longer have to be declared — apps that still declare them pointing at `SceneDelegate` keep working. + +Without the manifest the app runs the pre-scene `UIApplication` lifecycle with a single window, and everything below except opening a second window still applies. :::tip iPhone and UIScene -Even on iPhone, adding the manifest switches your app to UIScene lifecycle. Xcode may show warnings like “UIScene lifecycle will soon be required” — using the manifest addresses this. +Even on iPhone, adding the manifest switches your app to the UIScene lifecycle. Xcode may warn that "UIScene lifecycle will soon be required" — the manifest addresses that. ::: -## How it works in NativeScript +### Android: allow a second activity instance + +`Application.openWindow()` launches your start activity into its own task, so its `launchMode` has to allow a second instance. The app template ships with: + +```xml + +``` + +`singleTask` (the template default before 9.1) and `singleInstance` hand the launch intent to the existing activity's `onNewIntent` instead of creating a window. `singleInstancePerTask` (API 31+) keeps single-task behavior for launcher and deep-link starts while allowing extra windows; `standard` works too. -When the scene manifest is present: +:::warning Experimental on Android +`Application.openWindow()` is experimental on Android. Behavior around OEM recents, task management and window restoration after process death still varies by device. +::: -- A `SceneDelegate` (exposed to iOS as `SceneDelegate`) implements `UIWindowSceneDelegate` to integrate scenes with NativeScript’s application runtime. -- A UIWindow is created per `UIWindowScene` and mapped internally, preserving compatibility with traditional app lifecycle APIs. -- NativeScript fires scene-specific events and forwards core application lifecycle events from a primary scene to maintain compatibility with existing code. +## Providing content for a window -### Scene lifecycle events +Before 9.1, the first window's UI came from the `launch` event or the application main entry, and a second window had no way to get different content. Those two jobs are now separate: -Use the `SceneEvents` constants to subscribe to scene lifecycle changes: +- **`ready`** fires once per JavaScript context, as soon as the context is initialized. It is never deferred, so it also fires on a background launch that opens no window. Do app-level initialization here. +- **`Application.setWindowContentResolver()`** supplies the UI for each window that needs content. ```ts -export const SceneEvents = { - sceneWillConnect: 'sceneWillConnect', - sceneDidActivate: 'sceneDidActivate', - sceneWillResignActive: 'sceneWillResignActive', - sceneWillEnterForeground: 'sceneWillEnterForeground', - sceneDidEnterBackground: 'sceneDidEnterBackground', - sceneDidDisconnect: 'sceneDidDisconnect', - sceneContentSetup: 'sceneContentSetup', -} +import { Application } from '@nativescript/core' +import type { WindowContentRequest } from '@nativescript/core' + +Application.on('ready', () => { + // app-wide initialization - no window exists yet +}) + +Application.setWindowContentResolver((request: WindowContentRequest) => { + // Hand the primary window - and anything the system restored on its own - + // back to the application main entry. + if (request.isPrimary) { + return undefined + } + + switch (request.data?.kind) { + case 'inspector': + return 'pages/inspector-page' + default: + return createDetailPage(request.window) + } +}) + +Application.run({ moduleName: 'app-root' }) +``` + +The resolver receives a `WindowContentRequest`: + +| Property | Description | +| ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `window` | The `NativeWindow` that needs content. | +| `isPrimary` | Whether this is the application's primary window. | +| `data` | The payload passed to `openWindow()` — `NSUserActivity.userInfo` on iOS, intent extras on Android. | +| `ios.connectionOptions` | The scene's `UISceneConnectionOptions`. | +| `android.intent`, `android.savedInstanceState` | The launch intent and saved state for the activity. | + +What you return decides what happens next: + +| Return value | Result | +| ------------------------------------ | ---------------------------------------------------------------------- | +| A `View` | Becomes the window content. | +| A `NavigationEntry` or a module name | Built, then used as the window content. | +| `null` | You take ownership — set the content later with `window.setContent()`. | +| `undefined` | Falls back to the application main entry. | + +Keep the resolver installed for the life of the process: a window that detaches and re-attaches (scene reconnect, activity recreation) asks for its content again, long after the code that installed the resolver may have moved on. + +Content can also be set at any time, from anywhere: + +```ts +const window = Application.getWindowById(id) +window.setContent(new DetailPage()) ``` -Event payloads include scene and window references: +`setContent()` replaces any content set earlier, tears the previous root view down and raises `contentLoaded`. A view that is already the root of another window is released from that window first — the view itself is left intact. + +### Startup order + +Bringing a window up follows a fixed order you can rely on: + +`ready` → `windowOpen` → the raw platform event (`sceneWillConnect` / `activityCreated`) → content resolution → `contentLoaded` → `activate` and `displayed`. + +## Opening and closing windows ```ts -/** iOS event data for UIScene lifecycle (iOS 13+). */ -export interface SceneEventData extends ApplicationEventData { - /** The UIWindowScene instance associated with this event. */ - scene?: UIWindowScene - /** The UIWindow for this scene (if applicable). */ - window?: UIWindow - /** Scene connection options (for sceneWillConnect). */ - connectionOptions?: UISceneConnectionOptions - /** Additional user info from the notification. */ - userInfo?: NSDictionary -} +import { Application } from '@nativescript/core' + +Application.openWindow({ data: { kind: 'inspector' } }) ``` -### iOSApplication scene APIs +The `data` payload travels to the new window and arrives as `request.data` in the content resolver — serialized into `NSUserActivity.userInfo` on iOS, added as intent extras on Android. -When UIScene is active, `Application.ios` exposes helpers for inspecting and controlling scenes and windows: +Multi-window is a device capability rather than a platform one: an iPhone and an iPad run the same iOS build, but only one of them can show two scenes. Check before offering the option: -- `supportsScenes(): boolean` — iOS supports UIScene (iOS 13+) -- `supportsMultipleScenes(): boolean` — app can present multiple scenes/windows (iPadOS; typically false on iPhone and some simulators) -- `getAllWindows(): UIWindow[]` — all app windows across scenes -- `getAllScenes(): UIScene[]` — all attached scenes -- `getWindowScenes(): UIWindowScene[]` — filtered to window scenes -- `getPrimaryWindow(): UIWindow | undefined` — the primary window (for compatibility) -- `getPrimaryScene(): UIWindowScene | undefined` — the primary scene -- `isUsingSceneLifecycle(): boolean` — whether UIScene lifecycle is active -- `setWindowRootView(window: UIWindow, view: View): void` — set NativeScript root view for a given scene’s window +```ts +import { Application, isIOS } from '@nativescript/core' -## Usage +const canOpenWindows = isIOS + ? Application.ios.supportsScenes() && Application.ios.supportsMultipleScenes() + : true +``` -### Listen to scene events +Close a window through the window itself: ```ts -import { Application, SceneEvents } from '@nativescript/core' +window.close() +``` -Application.on(SceneEvents.sceneWillConnect, (args) => { - console.log('Scene connecting:', args.scene) - console.log('Window:', args.window) - console.log('Connection options:', args.connectionOptions) -}) +The primary window refuses to close. A window that does close raises `close` exactly once, is dropped from the registry, and then has every listener on it cleared — so handlers registered on a window never outlive it, and a closed window instance must not be reused. -Application.on(SceneEvents.sceneDidActivate, (args) => { - console.log('Scene active:', args.scene) -}) +## Working with windows -Application.on(SceneEvents.sceneWillResignActive, (args) => { - console.log('Scene will resign active:', args.scene) -}) +| API | Description | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Application.primaryWindow` | The primary window. | +| `Application.activeWindow` | The window the user is interacting with — the one that activated most recently and is still attached. Falls back to the primary window. | +| `Application.getWindows(role?)` | Windows filtered by role, defaulting to the view-carrying roles `application` and `embedded`. Pass `'all'` to enumerate every registered surface. | +| `Application.getWindowById(id)` | A registered window by id. | +| `Application.openWindow(options?)` | Opens a new window. | -Application.on(SceneEvents.sceneWillEnterForeground, (args) => { - console.log('Scene will enter foreground:', args.scene) -}) +To go from a view to the window hosting it, use `getNativeWindow()`. It walks up to the root view — through the presenting view of any modal on the way — so a view re-parented into another window reports the window it moved to: -Application.on(SceneEvents.sceneDidEnterBackground, (args) => { - console.log('Scene entered background:', args.scene) -}) +```ts +import { Frame } from '@nativescript/core' +import type { EventData, View } from '@nativescript/core' -Application.on(SceneEvents.sceneDidDisconnect, (args) => { - console.log('Scene disconnected:', args.scene) -}) +export function onTap(args: EventData) { + const view = args.object as View -Application.on(SceneEvents.sceneContentSetup, (args) => { - // Create and attach NativeScript View content for the new scene here - // See "Provide scene-specific UI" section below - setupSceneContent(args) + // Navigate the frame of the window this button lives in, not the app's topmost frame. + Frame.topmost(view.getNativeWindow()).navigate('pages/details') +} +``` + +`Frame.topmost()` takes an optional window and defaults to `Application.activeWindow`. + +## Window lifecycle events + +Subscribe on a `NativeWindow` for events scoped to that one window. The `NativeWindowEvents` constants carry the names: + +| Event | Raised when | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `attached` | A native surface is bound to the window — on the first connect and on every re-attach. | +| `detached` | The native surface goes away while the window session stays alive (iOS scene disconnect, Android activity recreation). | +| `activate` / `deactivate` | The window gains or loses focus. | +| `background` / `foreground` | The window enters the background or comes back to the foreground. | +| `contentLoaded` | The root view content is set or changed. | +| `displayed` | The window content has been displayed for the first time. | +| `close` | The window session ends for good. Fires exactly once. | +| `orientationChanged` | This window's orientation changes. | +| `systemAppearanceChanged` | This window switches between light and dark. | +| `layoutDirectionChanged` | This window switches between `ltr` and `rtl`. | + +```ts +import { Application, NativeWindowEvents } from '@nativescript/core' + +const window = Application.activeWindow + +window.on(NativeWindowEvents.orientationChanged, (args) => { + console.log(`${args.window.id} is now ${args.newValue}`) }) + +// No unsubscribe needed - the framework drops every listener on a window +// right after its `close` event. ``` -### Inspect and manage windows +:::warning detached is not closed +A scene disconnect or an activity recreation raises `detached`, not `close`. The window stays registered, keeps its id and keeps its listeners, and the same instance is handed back when a surface re-attaches. Only tear down per-window state on `close`. +::: + +`displayed` currently fires for the primary window only. + +## Application-level window events + +The `WindowEvents` constants cover the registry as a whole: ```ts -import { Application } from '@nativescript/core' +import { Application, WindowEvents } from '@nativescript/core' -if (Application.ios.supportsScenes()) { - const windows = Application.ios.getAllWindows() - const scenes = Application.ios.getWindowScenes() - const primaryWindow = Application.ios.getPrimaryWindow() +Application.on(WindowEvents.windowOpen, (args) => { + console.log(`opened ${args.window.id}`) +}) +Application.on(WindowEvents.windowClose, (args) => { + console.log(`closed ${args.window.id}`) +}) +Application.on(WindowEvents.primaryWindowChanged, (args) => { + console.log(`${args.window.id} is now primary`) +}) +``` - console.log(`App has ${windows.length} windows`) - console.log(`App has ${scenes.length} scenes`) - console.log('Primary window:', primaryWindow) +Two application events changed meaning when more than one window is involved: - if (Application.ios.isUsingSceneLifecycle()) { - console.log('Using UIScene lifecycle') - } -} else { - console.log('Single-window app lifecycle in effect') -} +- **`suspend` / `resume`** reflect whole-app state — they are raised once the app itself leaves or returns to the foreground, not when an individual window backgrounds. Use a window's `background` and `foreground` events to track one window. +- **`exit`** is raised on Android when the _last_ window finishes (the process may stay alive); on iOS it still means process termination. + +## Per-window orientation, appearance and direction + +Two windows can legitimately disagree about all three, so each window reports its own: + +```ts +window.orientation() // 'portrait' | 'landscape' | 'unknown' +window.systemAppearance() // 'light' | 'dark' | null +window.layoutDirection() // 'ltr' | 'rtl' | null ``` -### Provide scene-specific UI +Values are read from the native surface while the window is attached; a detached window reports the last value it saw. A read that catches a change the platform has not reported yet also raises the matching `*Changed` event, so a change is never swallowed. + +The CSS classes follow the same rule. `ns-portrait` / `ns-landscape`, `ns-light` / `ns-dark` and `ns-ltr` / `ns-rtl` are applied to each window's root view and to the modals presented over it, instead of living in the process-wide system class list. `CSSUtils.getRootViewCssClasses()` no longer returns them. + +## Reaching the native window ```ts -import { Application, Page, Utils } from '@nativescript/core' +const uiWindow = window.ios?.uiWindow // UIWindow +const scene = window.ios?.scene // UIWindowScene, absent on the pre-scene lifecycle +const activity = window.android?.activity // AppCompatActivity +``` -function createPageForScene(scene: UIWindowScene, window: UIWindow): Page { - // Construct any NativeScript view hierarchy here - const page = new Page() - // ... add content - return page -} +The platform bridges on `Application` are aggregate APIs: they fire for every window, and `args.window` identifies which one. -export function setupSceneContent(args: SceneEventData) { - // Optionally distinguish scenes by an id when opening a new window - // (e.g., via NSUserActivity userInfo) - let nsViewId: string | undefined - if (args.connectionOptions?.userActivities?.count > 0) { - const activity = - args.connectionOptions.userActivities.allObjects.objectAtIndex( - 0, - ) as NSUserActivity - nsViewId = Utils.dataDeserialize(activity.userInfo).id - } +```ts +Application.ios.on('sceneWillConnect', (args) => { + // args.window is the NativeWindow, args.scene the UIWindowScene, + // args.uiWindow the native UIWindow +}) - let page: Page - switch (nsViewId) { - case 'newSceneBasic': - page = createPageForScene(args.scene, args.window) - break - case 'newSceneAlt': - page = createPageForScene(args.scene, args.window) // replace with alt page - break - default: - page = createPageForScene(args.scene, args.window) - } +Application.android.on('activityResult', (args) => { + // args.window is the NativeWindow the activity belongs to +}) +``` - Application.ios.setWindowRootView(args.window, page) -} +Every one of these events is also available on a single `NativeWindow`, which is usually what you want in a multi-window app: + +```ts +window.on('activityResult', (args) => { + /* only this window's results */ +}) ``` -## Custom SceneDelegate (advanced) +## Advanced: other scene roles on iOS + +NativeScript auto-manages scenes with the `UIWindowSceneSessionRoleApplication` role. To handle another role — CarPlay, an external display — return a configuration from `Application.ios.onSceneConfiguration`: -NativeScript ships a default `SceneDelegate` that integrates UIScene with the runtime and event system. If your application needs custom scene delegate behavior, you can provide your own implementation named `SceneDelegate` in your app and wire additional logic. Ensure that you continue to create a `UIWindow` per `UIWindowScene` and set the NativeScript root view to keep app behavior consistent. Most apps should prefer the default delegate. +```ts +Application.ios.onSceneConfiguration = (app, session, options) => { + if (session.role === CPTemplateApplicationSceneSessionRoleApplication) { + const config = UISceneConfiguration.configurationWithNameSessionRole( + 'CarPlay', + session.role, + ) + config.delegateClass = MyCarPlaySceneDelegate + return config + } -## Compatibility and behavior + // Let NativeScript handle everything else. + return null +} +``` -- Backwards compatibility: on devices or builds without a scene manifest, the traditional single-window lifecycle is used and existing apps continue to work unchanged. -- Primary scene: for compatibility, NativeScript forwards core app lifecycle events (e.g., didBecomeActive) from the primary scene. -- Multiple scenes: `supportsMultipleScenes()` is typically only true on physical iPadOS devices; it may return false on iPhone and some simulators. +If your own application delegate implements `applicationConfigurationForConnectingSceneSessionOptions` or `applicationDidDiscardSceneSessions`, NativeScript will not install its defaults over yours — forward to them so window bookkeeping stays correct: -## Migration guidance +```ts +applicationConfigurationForConnectingSceneSessionOptions(app, session, options) { + if (session.role === myCustomRole) { + return myConfig + } + return Application.ios.defaultSceneConfiguration(app, session, options) +} +``` -Existing apps do not need to change code to adopt UIScene. To enable multi-window capabilities and Scene events: +## Migrating from NativeScript 9.0 -1. Add the scene manifest to `Info.plist` (see above). -2. Listen to `SceneEvents` to tailor behavior per window. -3. If you open additional windows, set their root views with `Application.ios.setWindowRootView` during `sceneContentSetup`. +Existing single-window apps keep working without changes. If you used the 9.0 scene APIs, the pattern of listening for `sceneContentSetup` and calling `Application.ios.setWindowRootView()` is replaced by the content resolver and `window.setContent()`, which work the same way on both platforms. -## Troubleshooting +| 9.0 | 9.1 | +| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `NativeWindow.iosWindow`, and its `window` property | `window.ios?.uiWindow` and `window.ios?.scene` | +| `NativeWindow.androidWindow` | `window.android?.activity` | +| `SceneEventData.window` was the native `UIWindow` | `window` is now the `NativeWindow`; the native window moved to `uiWindow`. A `window` payload key always means a `NativeWindow`. | +| `getWindows()` returned every surface | Role-filtered, defaulting to `application` + `embedded`. Use `getWindows('all')` for every registered surface. | +| Android `exit` fired for any finishing activity | Fires only when the last window finishes. iOS `exit` is unchanged. | +| `suspend` / `resume` tracked the single window | Reflect whole-app state. Use a window's `background` / `foreground` for one window. | +| Scene disconnect and activity recreation destroyed the window | They raise `detached`; the window stays registered and its listeners survive. | +| Listeners on a window outlived it | A window clears its listeners after `close`. Do not reuse a closed window instance. | +| `CSSUtils.getRootViewCssClasses()` included orientation, appearance and direction classes | These are per-window; read them from the window. | -- “UIScene lifecycle will soon be required” in Xcode: add the scene manifest to `Info.plist` to adopt UIScene. -- No multiple windows on iPhone: expected; iPhone uses UIScene lifecycle but doesn’t expose multi-window UX to users. -- `supportsMultipleScenes()` returns false on simulator: test on a physical iPad where multi-window is supported. +Deprecated but still working: -## Summary +| Deprecated | Use instead | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| The `launch` event | `ready` plus `Application.setWindowContentResolver()` | +| `SceneEvents.sceneContentSetup` | The `windowOpen` event and `window.setContent()` | +| `SceneEvents` | `NativeWindowEvents` | +| `Application.ios.shouldDelayLaunchEvent` | Now a no-op | +| `Application.orientation()`, `systemAppearance()`, `layoutDirection()` | The equivalent method on a window; they delegate to `primaryWindow` | +| `getAllWindows()`, `getAllScenes()`, `getWindowScenes()`, `getPrimaryWindow()`, `getPrimaryScene()` | `Application.getWindows()`, `Application.primaryWindow` | -With UIScene enabled, NativeScript gives you: +The `activity*` and `scene*` bridges on `Application` are **not** deprecated — they are permanent aggregate APIs that fire for every window. -- Scene-aware events for window lifecycle handling -- APIs to inspect scenes and windows and set scene-specific root views -- Backwards-compatible behavior for apps that haven’t yet adopted scenes +## Troubleshooting -Use the examples above as a starting point to build multi-window workflows on iPadOS and visionOS while keeping your app ready for the future UIScene requirement on iOS. +- **"UIScene lifecycle will soon be required" in Xcode** — add the scene manifest to `Info.plist`. +- **`supportsMultipleScenes()` returns false** — the device allows a single scene at a time. Either it is an iPhone, or `UIApplicationSupportsMultipleScenes` is missing from `Info.plist`. Test on an iPad. +- **`openWindow()` on Android brings the existing window forward instead of opening one** — the start activity's `launchMode` is `singleTask` or `singleInstance`. Switch to `singleInstancePerTask` or `standard`. +- **A second window shows the main entry instead of your UI** — the content resolver returned `undefined`, or the payload you branched on did not arrive. Log `request.data` to check what the platform carried across. +- **`exit` never fires on Android** — a detached window that never re-attaches keeps the registry non-empty and suppresses `exit`. Suppression is intentional for the ordinary recreation case, and there is no clean way to tell a window that is gone for good from one that is about to re-attach.