Skip to content

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

Open

Description

@rafaalbuquerquee

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:

// 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:

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:

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:

// 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions