From 8fcdc321e0970b19fc6fad7ba3b3245994c9a973 Mon Sep 17 00:00:00 2001 From: Suryansh0910 Date: Fri, 5 Dec 2025 00:05:25 +0530 Subject: [PATCH] fix(android): preserve file chooser callback across activity recreation Fixes #8246 When the Android system destroys and recreates the Activity while a file picker is open (due to backgrounding or memory pressure), the ValueCallback reference is lost, causing file uploads to fail silently. This fix stores the file chooser callbacks in static variables so they survive activity recreation. When the activity result is received: - If the instance callback exists, it's used normally - If the instance callback is null, the static pending callback is used Changes: - Added static variables to store pending file path callback, image URI, and file chooser type - Added FileChooserType enum to track which picker was opened - Added handlePendingFileChooserResult() to handle results when activity was recreated - Added clearPendingFileChooserState() to clean up static state - Updated showImageCapturePicker(), showVideoCapturePicker(), and showFilePicker() to store callbacks before launching --- .../getcapacitor/BridgeWebChromeClient.java | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java index 6ca1c8a838..6b9f603d79 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java +++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java @@ -46,6 +46,17 @@ private interface ActivityResultListener { void onActivityResult(ActivityResult result); } + // Static variables to store pending file chooser state to survive activity recreation + private static ValueCallback pendingFilePathCallback; + private static Uri pendingImageFileUri; + private static FileChooserType pendingFileChooserType; + + private enum FileChooserType { + IMAGE_CAPTURE, + VIDEO_CAPTURE, + FILE_PICKER + } + private ActivityResultLauncher permissionLauncher; private ActivityResultLauncher activityLauncher; private PermissionListener permissionListener; @@ -70,10 +81,70 @@ public BridgeWebChromeClient(Bridge bridge) { activityLauncher = bridge.registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), (result) -> { if (activityListener != null) { activityListener.onActivityResult(result); + } else if (pendingFilePathCallback != null) { + // Handle case where activity was recreated and instance callback is null + // Use the static callback instead + handlePendingFileChooserResult(result); } }); } + /** + * Handle file chooser result when the activity was recreated and instance callbacks were lost. + * This uses the static pending callback to deliver the result. + */ + private void handlePendingFileChooserResult(ActivityResult result) { + if (pendingFilePathCallback == null) { + return; + } + + try { + Uri[] uriResult = null; + if (result.getResultCode() == Activity.RESULT_OK) { + switch (pendingFileChooserType) { + case IMAGE_CAPTURE: + if (pendingImageFileUri != null) { + uriResult = new Uri[] { pendingImageFileUri }; + } + break; + case VIDEO_CAPTURE: + Intent videoData = result.getData(); + if (videoData != null && videoData.getData() != null) { + uriResult = new Uri[] { videoData.getData() }; + } + break; + case FILE_PICKER: + Intent fileData = result.getData(); + if (fileData != null) { + if (fileData.getClipData() != null) { + final int numFiles = fileData.getClipData().getItemCount(); + uriResult = new Uri[numFiles]; + for (int i = 0; i < numFiles; i++) { + uriResult[i] = fileData.getClipData().getItemAt(i).getUri(); + } + } else { + uriResult = WebChromeClient.FileChooserParams.parseResult(result.getResultCode(), fileData); + } + } + break; + } + } + pendingFilePathCallback.onReceiveValue(uriResult); + } finally { + // Clear the static state after handling + clearPendingFileChooserState(); + } + } + + /** + * Clear the static pending file chooser state. + */ + private static void clearPendingFileChooserState() { + pendingFilePathCallback = null; + pendingImageFileUri = null; + pendingFileChooserType = null; + } + /** * Render web content in `view`. * @@ -340,12 +411,19 @@ private boolean showImageCapturePicker(final ValueCallback filePathCallba return false; } takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri); + + // Store in static variables to survive activity recreation + pendingFilePathCallback = filePathCallback; + pendingImageFileUri = imageFileUri; + pendingFileChooserType = FileChooserType.IMAGE_CAPTURE; + activityListener = (activityResult) -> { Uri[] result = null; if (activityResult.getResultCode() == Activity.RESULT_OK) { result = new Uri[] { imageFileUri }; } filePathCallback.onReceiveValue(result); + clearPendingFileChooserState(); }; activityLauncher.launch(takePictureIntent); @@ -359,12 +437,18 @@ private boolean showVideoCapturePicker(final ValueCallback filePathCallba return false; } + // Store in static variables to survive activity recreation + pendingFilePathCallback = filePathCallback; + pendingImageFileUri = null; + pendingFileChooserType = FileChooserType.VIDEO_CAPTURE; + activityListener = (activityResult) -> { Uri[] result = null; if (activityResult.getResultCode() == Activity.RESULT_OK) { result = new Uri[] { activityResult.getData().getData() }; } filePathCallback.onReceiveValue(result); + clearPendingFileChooserState(); }; activityLauncher.launch(takeVideoIntent); @@ -383,6 +467,12 @@ private void showFilePicker(final ValueCallback filePathCallback, FileCho intent.setType(validTypes[0]); } } + + // Store in static variables to survive activity recreation + pendingFilePathCallback = filePathCallback; + pendingImageFileUri = null; + pendingFileChooserType = FileChooserType.FILE_PICKER; + try { activityListener = (activityResult) -> { Uri[] result; @@ -397,10 +487,12 @@ private void showFilePicker(final ValueCallback filePathCallback, FileCho result = WebChromeClient.FileChooserParams.parseResult(activityResult.getResultCode(), resultIntent); } filePathCallback.onReceiveValue(result); + clearPendingFileChooserState(); }; activityLauncher.launch(intent); } catch (ActivityNotFoundException e) { filePathCallback.onReceiveValue(null); + clearPendingFileChooserState(); } }