@capgo/capacitor-inappbrowser 8.15.0 → 8.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -574,9 +574,7 @@ public class CapgoInAppBrowserPlugin extends Plugin implements WebViewDialog.Per
574
574
  }
575
575
 
576
576
  // Send the result to WebView and clean up
577
- webViewDialog.mFilePathCallback.onReceiveValue(results);
578
- webViewDialog.mFilePathCallback = null;
579
- webViewDialog.tempCameraUri = null;
577
+ webViewDialog.completeLegacyFileChooserResult(results);
580
578
  }
581
579
  }
582
580
 
@@ -0,0 +1,57 @@
1
+ package ee.forgr.capacitor_inappbrowser;
2
+
3
+ import android.net.Uri;
4
+ import android.webkit.ValueCallback;
5
+
6
+ /**
7
+ * Request-scoped state for Android WebView file-chooser flows.
8
+ * Prevents overlapping chooser/camera requests from cross-delivering results.
9
+ */
10
+ final class FileChooserRequestSupport {
11
+
12
+ static final class FileChooserRequest {
13
+
14
+ private static long nextId = 0;
15
+
16
+ final long id;
17
+ final ValueCallback<Uri[]> callback;
18
+ final String acceptType;
19
+ final boolean multiple;
20
+ Uri tempCameraUri;
21
+
22
+ FileChooserRequest(ValueCallback<Uri[]> callback, String acceptType, boolean multiple) {
23
+ this.id = ++nextId;
24
+ this.callback = callback;
25
+ this.acceptType = acceptType;
26
+ this.multiple = multiple;
27
+ }
28
+ }
29
+
30
+ private FileChooserRequestSupport() {}
31
+
32
+ static boolean isActive(FileChooserRequest request, FileChooserRequest active) {
33
+ return request != null && request == active;
34
+ }
35
+
36
+ static void cancel(FileChooserRequest request) {
37
+ if (request != null && request.callback != null) {
38
+ request.callback.onReceiveValue(null);
39
+ }
40
+ }
41
+
42
+ static boolean completeIfActive(FileChooserRequest request, FileChooserRequest active, Uri[] results) {
43
+ if (!isActive(request, active)) {
44
+ return false;
45
+ }
46
+ request.callback.onReceiveValue(results);
47
+ return true;
48
+ }
49
+
50
+ static boolean cancelIfActive(FileChooserRequest request, FileChooserRequest active) {
51
+ if (!isActive(request, active)) {
52
+ return false;
53
+ }
54
+ request.callback.onReceiveValue(null);
55
+ return true;
56
+ }
57
+ }
@@ -412,6 +412,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
412
412
  public static final int FILE_CHOOSER_REQUEST_CODE = 1000;
413
413
  public ValueCallback<Uri> mUploadMessage;
414
414
  public ValueCallback<Uri[]> mFilePathCallback;
415
+ FileChooserRequestSupport.FileChooserRequest activeFileChooserRequest;
415
416
  private boolean openWebViewResolved;
416
417
  private boolean isDismissing = false;
417
418
  private PermissionRequest pendingCameraLaunchPermissionRequest;
@@ -2348,12 +2349,9 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2348
2349
  );
2349
2350
 
2350
2351
  // Check if the file chooser is already open
2351
- if (mFilePathCallback != null) {
2352
- mFilePathCallback.onReceiveValue(null);
2353
- mFilePathCallback = null;
2354
- }
2355
-
2356
- mFilePathCallback = filePathCallback;
2352
+ final boolean isMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE;
2353
+ beginFileChooserRequest(filePathCallback, acceptType, isMultiple);
2354
+ final FileChooserRequestSupport.FileChooserRequest request = activeFileChooserRequest;
2357
2355
 
2358
2356
  // Direct check for capture attribute in URL (fallback method)
2359
2357
  boolean isCaptureInUrl;
@@ -2428,12 +2426,16 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2428
2426
  """;
2429
2427
 
2430
2428
  webView.evaluateJavascript(js, (value) -> {
2429
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2430
+ return;
2431
+ }
2432
+
2431
2433
  Log.d("InAppBrowser", "Capture attribute JS result: " + value);
2432
2434
 
2433
2435
  // If we already found capture in URL, use that directly
2434
2436
  if (isCaptureInUrl) {
2435
2437
  Log.d("InAppBrowser", "Using capture from URL: " + captureMode);
2436
- launchCamera(captureMode.equals("user"));
2438
+ launchCamera(captureMode.equals("user"), request);
2437
2439
  return;
2438
2440
  }
2439
2441
 
@@ -2444,7 +2446,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2444
2446
  Log.d("InAppBrowser", "Found capture attribute: " + captureValue);
2445
2447
 
2446
2448
  if (!captureValue.isEmpty()) {
2447
- activity.runOnUiThread(() -> launchCamera(captureValue.equals("user")));
2449
+ activity.runOnUiThread(() -> launchCamera(captureValue.equals("user"), request));
2448
2450
  return;
2449
2451
  }
2450
2452
  }
@@ -2452,6 +2454,10 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2452
2454
  // Look for hints in the web page source
2453
2455
  Log.d("InAppBrowser", "Looking for camera hints in page content");
2454
2456
  webView.evaluateJavascript("(function() { return document.documentElement.innerHTML; })()", (htmlSource) -> {
2457
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2458
+ return;
2459
+ }
2460
+
2455
2461
  if (htmlSource != null && htmlSource.length() > 10) {
2456
2462
  boolean hasCameraOrSelfieKeyword =
2457
2463
  htmlSource.contains("capture=") || htmlSource.contains("camera") || htmlSource.contains("selfie");
@@ -2464,25 +2470,21 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2464
2470
  (currentUrl.contains("selfie") || currentUrl.contains("camera") || currentUrl.contains("photo"))
2465
2471
  ) {
2466
2472
  Log.d("InAppBrowser", "URL suggests camera usage, launching camera");
2467
- activity.runOnUiThread(() -> launchCamera(currentUrl.contains("selfie")));
2473
+ activity.runOnUiThread(() -> launchCamera(currentUrl.contains("selfie"), request));
2468
2474
  return;
2469
2475
  }
2470
2476
  }
2471
2477
 
2472
2478
  // If all detection methods fail, fall back to regular file picker
2473
2479
  Log.d("InAppBrowser", "No capture attribute detected, using file picker");
2474
- openFileChooser(
2475
- filePathCallback,
2476
- acceptType,
2477
- fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE
2478
- );
2480
+ openFileChooser(request);
2479
2481
  });
2480
2482
  });
2481
2483
  return true;
2482
2484
  }
2483
2485
 
2484
2486
  // For non-image types, use regular file picker
2485
- openFileChooser(filePathCallback, acceptType, fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE);
2487
+ openFileChooser(request);
2486
2488
  return true;
2487
2489
  }
2488
2490
 
@@ -2490,7 +2492,11 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2490
2492
  * Launch the camera app for capturing images
2491
2493
  * @param useFrontCamera true to use front camera, false for back camera
2492
2494
  */
2493
- private void launchCamera(boolean useFrontCamera) {
2495
+ private void launchCamera(boolean useFrontCamera, FileChooserRequestSupport.FileChooserRequest request) {
2496
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2497
+ return;
2498
+ }
2499
+
2494
2500
  Log.d("InAppBrowser", "Launching camera, front camera: " + useFrontCamera);
2495
2501
 
2496
2502
  // First check if we have camera permission
@@ -2514,8 +2520,11 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2514
2520
  Log.d("InAppBrowser", "Ignoring delayed camera permission grant during dismiss");
2515
2521
  return;
2516
2522
  }
2523
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2524
+ return;
2525
+ }
2517
2526
  // Permission granted, now launch the camera
2518
- launchCameraWithPermission(useFrontCamera);
2527
+ launchCameraWithPermission(useFrontCamera, request);
2519
2528
  }
2520
2529
 
2521
2530
  @Override
@@ -2525,9 +2534,12 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2525
2534
  Log.d("InAppBrowser", "Ignoring delayed camera permission denial during dismiss");
2526
2535
  return;
2527
2536
  }
2537
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2538
+ return;
2539
+ }
2528
2540
  // Permission denied, fall back to file picker
2529
2541
  Log.e("InAppBrowser", "Camera permission denied, falling back to file picker");
2530
- fallbackToFilePicker();
2542
+ fallbackToFilePicker(request);
2531
2543
  }
2532
2544
  };
2533
2545
 
@@ -2539,13 +2551,17 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2539
2551
  }
2540
2552
 
2541
2553
  // If we can't request permission, try launching directly
2542
- launchCameraWithPermission(useFrontCamera);
2554
+ launchCameraWithPermission(useFrontCamera, request);
2543
2555
  }
2544
2556
 
2545
2557
  /**
2546
2558
  * Launch camera after permission is granted
2547
2559
  */
2548
- private void launchCameraWithPermission(boolean useFrontCamera) {
2560
+ private void launchCameraWithPermission(boolean useFrontCamera, FileChooserRequestSupport.FileChooserRequest request) {
2561
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2562
+ return;
2563
+ }
2564
+
2549
2565
  try {
2550
2566
  Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
2551
2567
  if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null) {
@@ -2554,17 +2570,18 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2554
2570
  photoFile = createImageFile();
2555
2571
  } catch (IOException ex) {
2556
2572
  Log.e("InAppBrowser", "Error creating image file", ex);
2557
- fallbackToFilePicker();
2573
+ fallbackToFilePicker(request);
2558
2574
  return;
2559
2575
  }
2560
2576
 
2561
2577
  if (photoFile != null) {
2562
- tempCameraUri = FileProvider.getUriForFile(
2578
+ request.tempCameraUri = FileProvider.getUriForFile(
2563
2579
  activity,
2564
2580
  activity.getPackageName() + ".fileprovider",
2565
2581
  photoFile
2566
2582
  );
2567
- takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, tempCameraUri);
2583
+ syncFileChooserPublicFields();
2584
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, request.tempCameraUri);
2568
2585
 
2569
2586
  if (useFrontCamera) {
2570
2587
  takePictureIntent.putExtra("android.intent.extras.CAMERA_FACING", 1);
@@ -2580,15 +2597,23 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2580
2597
  "camera_capture",
2581
2598
  new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
2582
2599
  (result) -> {
2583
- if (result.getResultCode() == Activity.RESULT_OK) {
2584
- if (tempCameraUri != null) {
2585
- mFilePathCallback.onReceiveValue(new Uri[] { tempCameraUri });
2586
- }
2587
- } else {
2588
- mFilePathCallback.onReceiveValue(null);
2600
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2601
+ return;
2602
+ }
2603
+ Uri[] results = null;
2604
+ if (result.getResultCode() == Activity.RESULT_OK && request.tempCameraUri != null) {
2605
+ results = new Uri[] { request.tempCameraUri };
2606
+ }
2607
+ if (
2608
+ FileChooserRequestSupport.completeIfActive(
2609
+ request,
2610
+ activeFileChooserRequest,
2611
+ results
2612
+ )
2613
+ ) {
2614
+ request.tempCameraUri = null;
2615
+ clearActiveFileChooserRequest(request);
2589
2616
  }
2590
- mFilePathCallback = null;
2591
- tempCameraUri = null;
2592
2617
  }
2593
2618
  )
2594
2619
  .launch(takePictureIntent);
@@ -2598,26 +2623,27 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2598
2623
  }
2599
2624
  } catch (SecurityException e) {
2600
2625
  Log.e("InAppBrowser", "Security exception launching camera: " + e.getMessage(), e);
2601
- fallbackToFilePicker();
2626
+ fallbackToFilePicker(request);
2602
2627
  }
2603
2628
  } else {
2604
2629
  Log.e("InAppBrowser", "Failed to create photo URI, falling back to file picker");
2605
- fallbackToFilePicker();
2630
+ fallbackToFilePicker(request);
2606
2631
  }
2607
2632
  }
2608
2633
  } catch (Exception e) {
2609
2634
  Log.e("InAppBrowser", "Camera launch failed: " + e.getMessage(), e);
2610
- fallbackToFilePicker();
2635
+ fallbackToFilePicker(request);
2611
2636
  }
2612
2637
  }
2613
2638
 
2614
2639
  /**
2615
2640
  * Fall back to file picker when camera launch fails
2616
2641
  */
2617
- private void fallbackToFilePicker() {
2618
- if (mFilePathCallback != null) {
2619
- openFileChooser(mFilePathCallback, "image/*", false);
2642
+ private void fallbackToFilePicker(FileChooserRequestSupport.FileChooserRequest request) {
2643
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2644
+ return;
2620
2645
  }
2646
+ openFileChooser(request);
2621
2647
  }
2622
2648
 
2623
2649
  // Grant permissions for cam
@@ -4065,8 +4091,43 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
4065
4091
  }
4066
4092
  }
4067
4093
 
4068
- private void openFileChooser(ValueCallback<Uri[]> filePathCallback, String acceptType, boolean isMultiple) {
4069
- mFilePathCallback = filePathCallback;
4094
+ private void beginFileChooserRequest(ValueCallback<Uri[]> filePathCallback, String acceptType, boolean isMultiple) {
4095
+ if (activeFileChooserRequest != null) {
4096
+ FileChooserRequestSupport.cancel(activeFileChooserRequest);
4097
+ }
4098
+ activeFileChooserRequest = new FileChooserRequestSupport.FileChooserRequest(filePathCallback, acceptType, isMultiple);
4099
+ syncFileChooserPublicFields();
4100
+ }
4101
+
4102
+ private void clearActiveFileChooserRequest(FileChooserRequestSupport.FileChooserRequest request) {
4103
+ if (activeFileChooserRequest == request) {
4104
+ activeFileChooserRequest = null;
4105
+ syncFileChooserPublicFields();
4106
+ }
4107
+ }
4108
+
4109
+ private void syncFileChooserPublicFields() {
4110
+ mFilePathCallback = activeFileChooserRequest != null ? activeFileChooserRequest.callback : null;
4111
+ tempCameraUri = activeFileChooserRequest != null ? activeFileChooserRequest.tempCameraUri : null;
4112
+ }
4113
+
4114
+ void completeLegacyFileChooserResult(Uri[] results) {
4115
+ FileChooserRequestSupport.FileChooserRequest request = activeFileChooserRequest;
4116
+ if (request == null) {
4117
+ return;
4118
+ }
4119
+ if (FileChooserRequestSupport.completeIfActive(request, activeFileChooserRequest, results)) {
4120
+ request.tempCameraUri = null;
4121
+ clearActiveFileChooserRequest(request);
4122
+ }
4123
+ }
4124
+
4125
+ private void openFileChooser(FileChooserRequestSupport.FileChooserRequest request) {
4126
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
4127
+ return;
4128
+ }
4129
+ String acceptType = request.acceptType;
4130
+ boolean isMultiple = request.multiple;
4070
4131
  Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
4071
4132
  intent.addCategory(Intent.CATEGORY_OPENABLE);
4072
4133
 
@@ -4114,26 +4175,29 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
4114
4175
  "file_chooser",
4115
4176
  new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
4116
4177
  (result) -> {
4178
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
4179
+ return;
4180
+ }
4181
+ Uri[] results = null;
4117
4182
  if (result.getResultCode() == Activity.RESULT_OK) {
4118
4183
  Intent data = result.getData();
4119
4184
  if (data != null) {
4120
4185
  if (data.getClipData() != null) {
4121
4186
  // Handle multiple files
4122
4187
  int count = data.getClipData().getItemCount();
4123
- Uri[] results = new Uri[count];
4188
+ results = new Uri[count];
4124
4189
  for (int i = 0; i < count; i++) {
4125
4190
  results[i] = data.getClipData().getItemAt(i).getUri();
4126
4191
  }
4127
- mFilePathCallback.onReceiveValue(results);
4128
4192
  } else if (data.getData() != null) {
4129
4193
  // Handle single file
4130
- mFilePathCallback.onReceiveValue(new Uri[] { data.getData() });
4194
+ results = new Uri[] { data.getData() };
4131
4195
  }
4132
4196
  }
4133
- } else {
4134
- mFilePathCallback.onReceiveValue(null);
4135
4197
  }
4136
- mFilePathCallback = null;
4198
+ if (FileChooserRequestSupport.completeIfActive(request, activeFileChooserRequest, results)) {
4199
+ clearActiveFileChooserRequest(request);
4200
+ }
4137
4201
  }
4138
4202
  )
4139
4203
  .launch(Intent.createChooser(intent, "Select File"));
@@ -4154,26 +4218,29 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
4154
4218
  "file_chooser",
4155
4219
  new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
4156
4220
  (result) -> {
4221
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
4222
+ return;
4223
+ }
4224
+ Uri[] results = null;
4157
4225
  if (result.getResultCode() == Activity.RESULT_OK) {
4158
4226
  Intent data = result.getData();
4159
4227
  if (data != null) {
4160
4228
  if (data.getClipData() != null) {
4161
4229
  // Handle multiple files
4162
4230
  int count = data.getClipData().getItemCount();
4163
- Uri[] results = new Uri[count];
4231
+ results = new Uri[count];
4164
4232
  for (int i = 0; i < count; i++) {
4165
4233
  results[i] = data.getClipData().getItemAt(i).getUri();
4166
4234
  }
4167
- mFilePathCallback.onReceiveValue(results);
4168
4235
  } else if (data.getData() != null) {
4169
4236
  // Handle single file
4170
- mFilePathCallback.onReceiveValue(new Uri[] { data.getData() });
4237
+ results = new Uri[] { data.getData() };
4171
4238
  }
4172
4239
  }
4173
- } else {
4174
- mFilePathCallback.onReceiveValue(null);
4175
4240
  }
4176
- mFilePathCallback = null;
4241
+ if (FileChooserRequestSupport.completeIfActive(request, activeFileChooserRequest, results)) {
4242
+ clearActiveFileChooserRequest(request);
4243
+ }
4177
4244
  }
4178
4245
  )
4179
4246
  .launch(Intent.createChooser(intent, "Select File"));
@@ -4184,9 +4251,8 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
4184
4251
  } catch (ActivityNotFoundException ex) {
4185
4252
  // If still failing, report error
4186
4253
  Log.e("InAppBrowser", "No app can handle file picker", ex);
4187
- if (mFilePathCallback != null) {
4188
- mFilePathCallback.onReceiveValue(null);
4189
- mFilePathCallback = null;
4254
+ if (FileChooserRequestSupport.cancelIfActive(request, activeFileChooserRequest)) {
4255
+ clearActiveFileChooserRequest(request);
4190
4256
  }
4191
4257
  }
4192
4258
  }
@@ -6258,11 +6324,11 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
6258
6324
  }
6259
6325
 
6260
6326
  // Clear any pending callbacks to prevent memory leaks
6261
- if (mFilePathCallback != null) {
6262
- mFilePathCallback.onReceiveValue(null);
6263
- mFilePathCallback = null;
6327
+ if (activeFileChooserRequest != null) {
6328
+ FileChooserRequestSupport.cancel(activeFileChooserRequest);
6329
+ activeFileChooserRequest = null;
6330
+ syncFileChooserPublicFields();
6264
6331
  }
6265
- tempCameraUri = null;
6266
6332
 
6267
6333
  // Clear file inputs for security/privacy before destroying WebView
6268
6334
  try {
@@ -415,7 +415,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
415
415
  case aware = "AWARE"
416
416
  case fakeVisible = "FAKE_VISIBLE"
417
417
  }
418
- private let pluginVersion: String = "8.15.0"
418
+ private let pluginVersion: String = "8.15.1"
419
419
  public let identifier = "CapgoInAppBrowserPlugin"
420
420
  public let jsName = "CapgoInAppBrowser"
421
421
  public let pluginMethods: [CAPPluginMethod] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-inappbrowser",
3
- "version": "8.15.0",
3
+ "version": "8.15.1",
4
4
  "description": "Capacitor plugin in app browser",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",