From d1e5f3c0b0d40fb68f06b9e7127f10f494eb26b3 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Sun, 20 Sep 2026 02:45:51 +0100 Subject: [PATCH] v4 docs: add Execution Context page Documents the ExecutionContext facade and what a background cold launch means for your code: the runtime boots but the interactive boot is held back, so no start route, no screen, no queue worker. Covers the lock state check, a background command example, all eleven facade methods, Android differences and how to fake the context in tests. Also adds a short section to the queue worker page explaining why the worker doesn't start during a background wake. Co-Authored-By: Claude Opus 5 (1M context) --- .../4/digging-deeper/execution-context.md | 266 ++++++++++++++++++ .../docs/mobile/4/digging-deeper/queues.md | 10 + 2 files changed, 276 insertions(+) create mode 100644 resources/views/docs/mobile/4/digging-deeper/execution-context.md diff --git a/resources/views/docs/mobile/4/digging-deeper/execution-context.md b/resources/views/docs/mobile/4/digging-deeper/execution-context.md new file mode 100644 index 000000000..0ff6cde9f --- /dev/null +++ b/resources/views/docs/mobile/4/digging-deeper/execution-context.md @@ -0,0 +1,266 @@ +--- +title: Execution Context +order: 15 +--- + +## Overview + +Your app doesn't only run because someone tapped its icon. iOS will **cold-launch the whole app into the +background** to run scheduled work — a background task, a silent push, a background fetch — routinely while the +phone is locked in someone's pocket. The process boots, Laravel boots, your command runs, and the app is +suspended again, all without anything ever appearing on screen. + +That's a very different world to the one your UI code assumes. There's no user to show a screen to, no +navigation to perform, and the device's protected files and keychain items may be unreadable until the phone is +unlocked. + +`ExecutionContext` is how your PHP code asks where it is: + +```php +use Native\Mobile\Facades\ExecutionContext; + +if (ExecutionContext::isHeadless()) { + // Woken for background work. Do the job; don't touch the UI. +} +``` + +## What NativePHP does for you + +You don't have to defend against a background launch yourself — NativePHP splits its own boot in two and gates +the second half: + +- The **runtime** always boots. PHP starts, your app is extracted, migrations run, the persistent Laravel + runtime comes up, and plugin callbacks fire. That's what your scheduled work needs to run at all. +- The **interactive boot** is held back. Your [start URL](../getting-started/configuration#start-url) is not + dispatched, no screen is mounted, and no web view is created until the app is genuinely on screen. + +So a background wake never runs your `/` route, never fires a component's `mount()`, and never triggers the +authentication middleware, polling or navigation that would normally follow. When the user later opens the app, +the interactive boot happens then — exactly once. + + + +## Checking the context + +Every read goes straight to the platform, so the value is always current — even inside a long-lived native +screen whose request has been open for minutes. + +### Is anyone looking? + +```php +use Native\Mobile\Facades\ExecutionContext; + +ExecutionContext::isHeadless(); // background launch that has never been on screen +ExecutionContext::isForeground(); // on screen (active or momentarily inactive) +ExecutionContext::isBackground(); // not on screen +ExecutionContext::isActive(); // frontmost and receiving events +``` + +`isHeadless()` is the one you usually want. It's true only for the case that actually matters: the system +started this process for background work and the user has never seen it. + +The difference between `isActive()` and `isForeground()` is worth knowing. An app that's on screen but not +receiving events — mid app-switcher, behind an incoming call banner, under a system permission alert — is +*inactive* but still *foreground*. Don't treat that as "the user has gone away". + +### Is the device unlocked? + +```php +if (! ExecutionContext::isProtectedDataAvailable()) { + // Encrypted files and keychain items can't be read right now. + return; +} +``` + +On iOS this is false whenever the device is locked with Data Protection engaged; on Android it's false before +the user's first unlock after a reboot. Either way it means the same thing: anything in +[secure storage](../plugins/core/secure-storage), and any file protected by the OS, is unreadable until the +device is unlocked. + +This matters most in background work, which is precisely when the phone is most likely to be locked. Reading a +token you can't decrypt will fail, so check first and let the system reschedule you. + + + +## Background work + + + +```shell +composer require nativephp/mobile-background-tasks +php artisan native:plugin:register nativephp/mobile-background-tasks +``` + +Then rebuild your app so the plugin's native code is compiled in — see +[Using Plugins](../plugins/using-plugins) for the full flow. + +Once a task is registered, the OS decides when to run it — and it will pick moments when your app is closed and +the phone is locked. Write the command so that's the normal case rather than the exception: + +```php +namespace App\Console\Commands; + +use Illuminate\Console\Command; +use Native\Mobile\Facades\ExecutionContext; + +class SyncInbox extends Command +{ + protected $signature = 'app:sync-inbox'; + + public function handle(): int + { + if (! ExecutionContext::isProtectedDataAvailable()) { + $this->info('Device locked; will retry on the next window.'); + + return self::SUCCESS; + } + + $messages = $this->pullMessages(); + + if (ExecutionContext::isHeadless()) { + // No UI to update — just persist and let the app read it on open. + $this->store($messages); + + return self::SUCCESS; + } + + // Running while the user is watching, so it's safe to notify them. + $this->store($messages); + $this->refreshVisibleScreen(); + + return self::SUCCESS; + } +} +``` + +A background window is short and can be cut off at any time. Do the work inline and persist as you go, rather +than dispatching a job and assuming something will pick it up — during a headless launch, nothing will until +the app is opened. + +## Methods + +All methods are called on the `Native\Mobile\Facades\ExecutionContext` facade. + +### `isHeadless()` + +Whether the system cold-launched this process for background work and it has never been on screen. The check to +reach for when you want to know "should I skip anything user-facing?". + +**Returns:** `bool` + +### `isForeground()` + +Whether the app is on screen. Covers both `active` and the transient `inactive` state. + +**Returns:** `bool` + +### `isBackground()` + +Whether the app is not on screen. The inverse of `isForeground()`. + +**Returns:** `bool` + +### `isActive()` + +Whether the app is frontmost and receiving events. + +**Returns:** `bool` + +### `isProtectedDataAvailable()` + +Whether OS-protected files and keychain items can be read. False on iOS while the device is locked, and on +Android before the user's first unlock after a reboot. + +**Returns:** `bool` + +### `launchedInBackground()` + +Whether the process was started by the system for background work, regardless of what has happened since. Stays +true even after the user opens the app — unlike `isHeadless()`, which becomes false at that point. + +**Returns:** `bool` + +### `hasBecomeActive()` + +Whether the app has been on screen at least once since the process started. + +**Returns:** `bool` + +### `interactiveBootStarted()` + +Whether NativePHP has run its interactive boot — the point at which the start route was dispatched and the +first screen created. False for the whole of a headless launch. + +**Returns:** `bool` + +### `state()` + +The raw lifecycle state: `'active'`, `'inactive'` or `'background'`. + +**Returns:** `string` + +### `launch()` + +Why the process started: `'foreground'` (someone opened the app) or `'background'` (the system woke it). + +**Returns:** `string` + +### `all()` + +The whole context in one array, if you'd rather read it as data — useful for logging what a background run +actually saw. + +**Returns:** `array` + +## Platform differences + +Background cold launches are an **iOS** behaviour. Android starts your app's runtime from its Activity, so +there's no equivalent headless launch: `isHeadless()` is always false there and `launch()` always reports +`'foreground'`. + +`isProtectedDataAvailable()` is meaningful on both, mapping to device lock state on iOS and to the direct-boot +user-unlocked state on Android. + +Writing the checks anyway costs nothing and keeps one code path across both platforms. + +## Testing + +There's no device involved — script the context like any other native call: + +```php +use Native\Mobile\Testing\Native; + +Native::fakeBridge()->respondTo('System.GetExecutionContext', [ + 'headless' => true, + 'protected_data_available' => false, +]); +``` + +Anything you leave out falls back to an ordinary interactive app, so you only state the part you're testing. +See [Native Events](../testing/native-events) for more on scripting bridge responses. + +## Notes + +- **Reads are never cached.** A native screen's request stays open for as long as the screen is on the stack, so + a value captured once would go stale. Call the facade at the moment you need the answer. +- **Old native shells report an interactive app.** If your app's native project predates this API, every read + falls back to the foreground defaults. Rebuild with `php artisan native:run` to get real answers. +- **Don't use it to hide UI.** `ExecutionContext` tells you whether UI is *possible*, not what to render. During + a headless launch there is no screen to hide. diff --git a/resources/views/docs/mobile/4/digging-deeper/queues.md b/resources/views/docs/mobile/4/digging-deeper/queues.md index f890c06d0..1c5461a81 100644 --- a/resources/views/docs/mobile/4/digging-deeper/queues.md +++ b/resources/views/docs/mobile/4/digging-deeper/queues.md @@ -81,6 +81,16 @@ NativePHP manages the worker lifecycle natively on both platforms. +### Background launches + +The worker starts when your app is **on screen**. When iOS cold-launches your app in the background to run +scheduled work, that second PHP runtime is deliberately not started — it's memory a background wake didn't ask +for, and paying for it there risks the system killing the process mid-task. + +Jobs you dispatch from background work are still written to the queue as normal; they're picked up the next time +the app is opened. If a background window needs work *finished* rather than queued, do it inline in the command. +See [Execution Context](execution-context) for how to tell which situation you're in. + ## Things to Note - The queue worker requires ZTS (Thread-Safe) PHP, which NativePHP includes by default.