#8592·capacitor

[Bug]: <input type="file"> result is silently dropped when the app process is killed while the chooser activity is in the foreground

Author: rafaalbuquerqueeCreated Sep 8, 2026Updated Sep 8, 2026
Labelstriage

Bug

When the Android system kills the app process while a file-chooser activity (camera or gallery) is in the foreground, the result is silently dropped on return. filePathCallback.onReceiveValue() is never called — not even with null — so the <input type="file"> is left permanently pending in a WebView that no longer has the page state anyway.

There is no exception, no log line, and nothing visible to the user: the app simply reappears and the photo never arrives.

Why it happens

In BridgeWebChromeClient (8.5.0), the callback lives only in memory:

java
// BridgeWebChromeClient.java:52
private ActivityResultListener activityListener;

// :344  — the lambda captures filePathCallback
activityListener = (activityResult) -> {
    Uri[] result = null;
    if (activityResult.getResultCode() == Activity.RESULT_OK) {
        result = new Uri[] { imageFileUri };
    }
    filePathCallback.onReceiveValue(result);   // :349
};

Neither activityListener nor filePathCallback is written to the Bundle. After process death the recreated BridgeWebChromeClient has activityListener == null, so the guard at :71 drops the incoming result:

java
if (activityListener != null) {
    activityListener.onActivityResult(result);
}

grep for filePathCallback and activityListener in Bridge.java returns 0 (positive control: lastPluginId returns 4).

The machinery already exists — the file chooser just doesn't use it

PluginCall does survive process death. Plugin.startActivityForResult (:173) registers the call for persistence before launching:

java
bridge.setPluginCallForLastActivity(call);   // :179
lastPluginCallId = call.getCallbackId();
bridge.saveCall(call);
activityResultLauncher.launch(intent);

and Bridge.saveInstanceState (:1086) persists it, with the comment in the source itself:

java
// If there was a last PluginCall for a started activity, we need to
// persist it so we can load it again in case our app gets terminated

The file chooser is not a PluginCall, so none of this applies to it.

Reproduction

  1. Android device under memory pressure (or adb shell am send-trim-memory / background process limit set to a low value).
  2. In a Capacitor app, tap an <input type="file" accept="image/*" capture="environment">.
  3. While the camera is in the foreground, the system kills the app process.
  4. Take the photo and return.

Expected: the result reaches the input, or at least onReceiveValue(null) is called so the page can react. Actual: nothing happens. The app is recreated and the result is discarded.

Field measurement

Reproduced on a Moto G54 5G (Android 15, 3.5 GB RAM, targetSdkVersion 36), 4 of 5 taps across two sessions. The system log shows the pattern clearly:

13:20:32.609  ActivityTaskManager: START u0 {act=android.media.action.IMAGE_CAPTURE ...} from uid <app>
13:20:34.342  ActivityManager: appDiedLocked: app=ProcessRecord{... com.example.app} isKilledByAm=false
13:20:34.342  ActivityManager: Process com.example.app (pid 27962) has died: prev LAST
13:20:42.993  ActivityManager: Start proc 32361:com.example.app
13:20:43.xxx  wm_on_activity_result_called: [..., RESUME_ACTIVITY]   <- the Activity DOES get the result

The activity result is delivered to the recreated Activity; it is the bridge that has nothing left to hand it to.

Frequency tracks memory pressure rather than anything in app code: in the window above, 113 processes were killed in 4.5 minutes (including the camera app itself, twice), versus 5.2/min in an earlier window where only 1 of 2 taps died.

Suggested direction

Persist enough to rebuild the chain across process death — at minimum the pending imageFileUri and a flag that a chooser was in flight — using the same saveInstanceState/restoreInstanceState path that PluginCall already uses. Even without restoring the callback, calling onReceiveValue(null) on a restored-but-orphaned chooser would let the web layer detect and report the loss instead of hanging silently.

Environment

  • @capacitor/android 8.5.0, @capacitor/core 8.5.0
  • compileSdk / targetSdk 36
  • Android 15, Moto G54 5G

Happy to test a patch.