@capgo/capacitor-inappbrowser 8.15.0 → 8.15.2
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.
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/CapgoInAppBrowserPlugin.java +1 -3
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/FileChooserAcceptSupport.java +111 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/FileChooserRequestSupport.java +53 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +133 -106
- package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
578
|
-
webViewDialog.mFilePathCallback = null;
|
|
579
|
-
webViewDialog.tempCameraUri = null;
|
|
577
|
+
webViewDialog.completeLegacyFileChooserResult(results);
|
|
580
578
|
}
|
|
581
579
|
}
|
|
582
580
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package ee.forgr.capacitor_inappbrowser;
|
|
2
|
+
|
|
3
|
+
import android.content.Intent;
|
|
4
|
+
import android.webkit.MimeTypeMap;
|
|
5
|
+
import java.util.LinkedHashSet;
|
|
6
|
+
import java.util.Locale;
|
|
7
|
+
import java.util.function.Predicate;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes WebView file-input accept attributes into Android file-chooser intents.
|
|
11
|
+
*/
|
|
12
|
+
final class FileChooserAcceptSupport {
|
|
13
|
+
|
|
14
|
+
private FileChooserAcceptSupport() {}
|
|
15
|
+
|
|
16
|
+
static String acceptEntryToMimeType(String accept) {
|
|
17
|
+
if (accept == null || accept.isEmpty() || accept.equals("undefined")) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
accept = accept.toLowerCase(Locale.ROOT);
|
|
21
|
+
if (accept.contains("/")) {
|
|
22
|
+
return accept;
|
|
23
|
+
}
|
|
24
|
+
if (!accept.startsWith(".")) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(accept.substring(1).toLowerCase(Locale.ROOT));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static LinkedHashSet<String> normalizeAcceptTypes(String[] acceptTypes) {
|
|
31
|
+
LinkedHashSet<String> mimeTypes = new LinkedHashSet<>();
|
|
32
|
+
if (acceptTypes != null) {
|
|
33
|
+
for (String entry : acceptTypes) {
|
|
34
|
+
if (entry == null) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
for (String part : entry.split(",")) {
|
|
38
|
+
String mime = acceptEntryToMimeType(part.trim());
|
|
39
|
+
if (mime != null) {
|
|
40
|
+
mimeTypes.add(mime);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (mimeTypes.contains("*/*")) {
|
|
46
|
+
mimeTypes.clear();
|
|
47
|
+
}
|
|
48
|
+
return mimeTypes;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static boolean isImageOnlyAcceptTypes(String[] acceptTypes) {
|
|
52
|
+
return matchesAllResolvedAcceptTypes(acceptTypes, (mime) -> mime.startsWith("image/"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static boolean isMediaOnlyAcceptTypes(String[] acceptTypes) {
|
|
56
|
+
return matchesAllResolvedAcceptTypes(
|
|
57
|
+
acceptTypes,
|
|
58
|
+
(mime) -> mime.startsWith("image/") || mime.startsWith("video/") || mime.startsWith("audio/")
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static boolean matchesAllResolvedAcceptTypes(String[] acceptTypes, Predicate<String> mimePredicate) {
|
|
63
|
+
boolean foundMatch = false;
|
|
64
|
+
if (acceptTypes == null) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
for (String entry : acceptTypes) {
|
|
68
|
+
if (entry == null) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (String part : entry.split(",")) {
|
|
72
|
+
String token = part.trim();
|
|
73
|
+
if (token.isEmpty() || token.equals("undefined")) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
String mime = acceptEntryToMimeType(token);
|
|
77
|
+
if (mime == null || mime.equals("*/*")) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (!mimePredicate.test(mime)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
foundMatch = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return foundMatch;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static boolean useGetContentAction(String[] acceptTypes) {
|
|
90
|
+
LinkedHashSet<String> mimeTypes = normalizeAcceptTypes(acceptTypes);
|
|
91
|
+
return isMediaOnlyAcceptTypes(acceptTypes) && mimeTypes.size() == 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static Intent createFileChooserIntent(String[] acceptTypes, boolean multiple) {
|
|
95
|
+
LinkedHashSet<String> mimeTypes = normalizeAcceptTypes(acceptTypes);
|
|
96
|
+
Intent intent = new Intent(useGetContentAction(acceptTypes) ? Intent.ACTION_GET_CONTENT : Intent.ACTION_OPEN_DOCUMENT);
|
|
97
|
+
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
|
98
|
+
|
|
99
|
+
if (mimeTypes.size() == 1) {
|
|
100
|
+
intent.setType(mimeTypes.iterator().next());
|
|
101
|
+
} else {
|
|
102
|
+
intent.setType("*/*");
|
|
103
|
+
if (!mimeTypes.isEmpty()) {
|
|
104
|
+
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toArray(new String[0]));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple);
|
|
109
|
+
return intent;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
final ValueCallback<Uri[]> callback;
|
|
15
|
+
final String[] acceptTypes;
|
|
16
|
+
final boolean multiple;
|
|
17
|
+
Uri tempCameraUri;
|
|
18
|
+
|
|
19
|
+
FileChooserRequest(ValueCallback<Uri[]> callback, String[] acceptTypes, boolean multiple) {
|
|
20
|
+
this.callback = callback;
|
|
21
|
+
this.acceptTypes = acceptTypes;
|
|
22
|
+
this.multiple = multiple;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private FileChooserRequestSupport() {}
|
|
27
|
+
|
|
28
|
+
static boolean isActive(FileChooserRequest request, FileChooserRequest active) {
|
|
29
|
+
return request != null && request == active;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static void cancel(FileChooserRequest request) {
|
|
33
|
+
if (request != null && request.callback != null) {
|
|
34
|
+
request.callback.onReceiveValue(null);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static boolean completeIfActive(FileChooserRequest request, FileChooserRequest active, Uri[] results) {
|
|
39
|
+
if (!isActive(request, active)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
request.callback.onReceiveValue(results);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static boolean cancelIfActive(FileChooserRequest request, FileChooserRequest active) {
|
|
47
|
+
if (!isActive(request, active)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
request.callback.onReceiveValue(null);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -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;
|
|
@@ -2322,21 +2323,14 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2322
2323
|
ValueCallback<Uri[]> filePathCallback,
|
|
2323
2324
|
FileChooserParams fileChooserParams
|
|
2324
2325
|
) {
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
fileChooserParams.getAcceptTypes() != null &&
|
|
2329
|
-
fileChooserParams.getAcceptTypes().length > 0 &&
|
|
2330
|
-
!TextUtils.isEmpty(fileChooserParams.getAcceptTypes()[0])
|
|
2331
|
-
) {
|
|
2332
|
-
acceptType = fileChooserParams.getAcceptTypes()[0];
|
|
2333
|
-
} else {
|
|
2334
|
-
acceptType = "*/*";
|
|
2326
|
+
String[] acceptTypes = fileChooserParams.getAcceptTypes();
|
|
2327
|
+
if (acceptTypes == null || acceptTypes.length == 0) {
|
|
2328
|
+
acceptTypes = new String[] { "*/*" };
|
|
2335
2329
|
}
|
|
2336
2330
|
|
|
2337
2331
|
// DEBUG: Log details about the file chooser request
|
|
2338
2332
|
Log.d("InAppBrowser", "onShowFileChooser called");
|
|
2339
|
-
Log.d("InAppBrowser", "Accept
|
|
2333
|
+
Log.d("InAppBrowser", "Accept types: " + Arrays.toString(acceptTypes));
|
|
2340
2334
|
Log.d("InAppBrowser", "Current URL: " + getUrl());
|
|
2341
2335
|
Log.d("InAppBrowser", "Original URL: " + (webView.getOriginalUrl() != null ? webView.getOriginalUrl() : "null"));
|
|
2342
2336
|
Log.d(
|
|
@@ -2348,12 +2342,9 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2348
2342
|
);
|
|
2349
2343
|
|
|
2350
2344
|
// Check if the file chooser is already open
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
mFilePathCallback = filePathCallback;
|
|
2345
|
+
final boolean isMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE;
|
|
2346
|
+
beginFileChooserRequest(filePathCallback, acceptTypes, isMultiple);
|
|
2347
|
+
final FileChooserRequestSupport.FileChooserRequest request = activeFileChooserRequest;
|
|
2357
2348
|
|
|
2358
2349
|
// Direct check for capture attribute in URL (fallback method)
|
|
2359
2350
|
boolean isCaptureInUrl;
|
|
@@ -2370,8 +2361,10 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2370
2361
|
isCaptureInUrl = false;
|
|
2371
2362
|
}
|
|
2372
2363
|
|
|
2373
|
-
// For image inputs, try to detect capture attribute using JavaScript
|
|
2374
|
-
|
|
2364
|
+
// For image-only inputs, try to detect capture attribute using JavaScript.
|
|
2365
|
+
// Mixed accept lists (e.g. "image/*,application/pdf") must skip the camera
|
|
2366
|
+
// path entirely — the camera can only produce images.
|
|
2367
|
+
if (FileChooserAcceptSupport.isImageOnlyAcceptTypes(acceptTypes)) {
|
|
2375
2368
|
// Check if HTML content contains capture attribute on file inputs (synchronous check)
|
|
2376
2369
|
webView.evaluateJavascript(
|
|
2377
2370
|
"document.querySelector('input[type=\"file\"][capture]') !== null",
|
|
@@ -2428,12 +2421,16 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2428
2421
|
""";
|
|
2429
2422
|
|
|
2430
2423
|
webView.evaluateJavascript(js, (value) -> {
|
|
2424
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2431
2428
|
Log.d("InAppBrowser", "Capture attribute JS result: " + value);
|
|
2432
2429
|
|
|
2433
2430
|
// If we already found capture in URL, use that directly
|
|
2434
2431
|
if (isCaptureInUrl) {
|
|
2435
2432
|
Log.d("InAppBrowser", "Using capture from URL: " + captureMode);
|
|
2436
|
-
launchCamera(captureMode.equals("user"));
|
|
2433
|
+
launchCamera(captureMode.equals("user"), request);
|
|
2437
2434
|
return;
|
|
2438
2435
|
}
|
|
2439
2436
|
|
|
@@ -2444,7 +2441,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2444
2441
|
Log.d("InAppBrowser", "Found capture attribute: " + captureValue);
|
|
2445
2442
|
|
|
2446
2443
|
if (!captureValue.isEmpty()) {
|
|
2447
|
-
activity.runOnUiThread(() -> launchCamera(captureValue.equals("user")));
|
|
2444
|
+
activity.runOnUiThread(() -> launchCamera(captureValue.equals("user"), request));
|
|
2448
2445
|
return;
|
|
2449
2446
|
}
|
|
2450
2447
|
}
|
|
@@ -2452,6 +2449,10 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2452
2449
|
// Look for hints in the web page source
|
|
2453
2450
|
Log.d("InAppBrowser", "Looking for camera hints in page content");
|
|
2454
2451
|
webView.evaluateJavascript("(function() { return document.documentElement.innerHTML; })()", (htmlSource) -> {
|
|
2452
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2455
2456
|
if (htmlSource != null && htmlSource.length() > 10) {
|
|
2456
2457
|
boolean hasCameraOrSelfieKeyword =
|
|
2457
2458
|
htmlSource.contains("capture=") || htmlSource.contains("camera") || htmlSource.contains("selfie");
|
|
@@ -2464,25 +2465,21 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2464
2465
|
(currentUrl.contains("selfie") || currentUrl.contains("camera") || currentUrl.contains("photo"))
|
|
2465
2466
|
) {
|
|
2466
2467
|
Log.d("InAppBrowser", "URL suggests camera usage, launching camera");
|
|
2467
|
-
activity.runOnUiThread(() -> launchCamera(currentUrl.contains("selfie")));
|
|
2468
|
+
activity.runOnUiThread(() -> launchCamera(currentUrl.contains("selfie"), request));
|
|
2468
2469
|
return;
|
|
2469
2470
|
}
|
|
2470
2471
|
}
|
|
2471
2472
|
|
|
2472
2473
|
// If all detection methods fail, fall back to regular file picker
|
|
2473
2474
|
Log.d("InAppBrowser", "No capture attribute detected, using file picker");
|
|
2474
|
-
openFileChooser(
|
|
2475
|
-
filePathCallback,
|
|
2476
|
-
acceptType,
|
|
2477
|
-
fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE
|
|
2478
|
-
);
|
|
2475
|
+
openFileChooser(request);
|
|
2479
2476
|
});
|
|
2480
2477
|
});
|
|
2481
2478
|
return true;
|
|
2482
2479
|
}
|
|
2483
2480
|
|
|
2484
2481
|
// For non-image types, use regular file picker
|
|
2485
|
-
openFileChooser(
|
|
2482
|
+
openFileChooser(request);
|
|
2486
2483
|
return true;
|
|
2487
2484
|
}
|
|
2488
2485
|
|
|
@@ -2490,7 +2487,11 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2490
2487
|
* Launch the camera app for capturing images
|
|
2491
2488
|
* @param useFrontCamera true to use front camera, false for back camera
|
|
2492
2489
|
*/
|
|
2493
|
-
private void launchCamera(boolean useFrontCamera) {
|
|
2490
|
+
private void launchCamera(boolean useFrontCamera, FileChooserRequestSupport.FileChooserRequest request) {
|
|
2491
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2494
2495
|
Log.d("InAppBrowser", "Launching camera, front camera: " + useFrontCamera);
|
|
2495
2496
|
|
|
2496
2497
|
// First check if we have camera permission
|
|
@@ -2514,8 +2515,11 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2514
2515
|
Log.d("InAppBrowser", "Ignoring delayed camera permission grant during dismiss");
|
|
2515
2516
|
return;
|
|
2516
2517
|
}
|
|
2518
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2517
2521
|
// Permission granted, now launch the camera
|
|
2518
|
-
launchCameraWithPermission(useFrontCamera);
|
|
2522
|
+
launchCameraWithPermission(useFrontCamera, request);
|
|
2519
2523
|
}
|
|
2520
2524
|
|
|
2521
2525
|
@Override
|
|
@@ -2525,9 +2529,12 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2525
2529
|
Log.d("InAppBrowser", "Ignoring delayed camera permission denial during dismiss");
|
|
2526
2530
|
return;
|
|
2527
2531
|
}
|
|
2532
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2528
2535
|
// Permission denied, fall back to file picker
|
|
2529
2536
|
Log.e("InAppBrowser", "Camera permission denied, falling back to file picker");
|
|
2530
|
-
fallbackToFilePicker();
|
|
2537
|
+
fallbackToFilePicker(request);
|
|
2531
2538
|
}
|
|
2532
2539
|
};
|
|
2533
2540
|
|
|
@@ -2539,13 +2546,17 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2539
2546
|
}
|
|
2540
2547
|
|
|
2541
2548
|
// If we can't request permission, try launching directly
|
|
2542
|
-
launchCameraWithPermission(useFrontCamera);
|
|
2549
|
+
launchCameraWithPermission(useFrontCamera, request);
|
|
2543
2550
|
}
|
|
2544
2551
|
|
|
2545
2552
|
/**
|
|
2546
2553
|
* Launch camera after permission is granted
|
|
2547
2554
|
*/
|
|
2548
|
-
private void launchCameraWithPermission(boolean useFrontCamera) {
|
|
2555
|
+
private void launchCameraWithPermission(boolean useFrontCamera, FileChooserRequestSupport.FileChooserRequest request) {
|
|
2556
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2549
2560
|
try {
|
|
2550
2561
|
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
|
2551
2562
|
if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
|
@@ -2554,17 +2565,18 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2554
2565
|
photoFile = createImageFile();
|
|
2555
2566
|
} catch (IOException ex) {
|
|
2556
2567
|
Log.e("InAppBrowser", "Error creating image file", ex);
|
|
2557
|
-
fallbackToFilePicker();
|
|
2568
|
+
fallbackToFilePicker(request);
|
|
2558
2569
|
return;
|
|
2559
2570
|
}
|
|
2560
2571
|
|
|
2561
2572
|
if (photoFile != null) {
|
|
2562
|
-
tempCameraUri = FileProvider.getUriForFile(
|
|
2573
|
+
request.tempCameraUri = FileProvider.getUriForFile(
|
|
2563
2574
|
activity,
|
|
2564
2575
|
activity.getPackageName() + ".fileprovider",
|
|
2565
2576
|
photoFile
|
|
2566
2577
|
);
|
|
2567
|
-
|
|
2578
|
+
syncFileChooserPublicFields();
|
|
2579
|
+
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, request.tempCameraUri);
|
|
2568
2580
|
|
|
2569
2581
|
if (useFrontCamera) {
|
|
2570
2582
|
takePictureIntent.putExtra("android.intent.extras.CAMERA_FACING", 1);
|
|
@@ -2580,15 +2592,23 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2580
2592
|
"camera_capture",
|
|
2581
2593
|
new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
|
|
2582
2594
|
(result) -> {
|
|
2583
|
-
if (
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2595
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
Uri[] results = null;
|
|
2599
|
+
if (result.getResultCode() == Activity.RESULT_OK && request.tempCameraUri != null) {
|
|
2600
|
+
results = new Uri[] { request.tempCameraUri };
|
|
2601
|
+
}
|
|
2602
|
+
if (
|
|
2603
|
+
FileChooserRequestSupport.completeIfActive(
|
|
2604
|
+
request,
|
|
2605
|
+
activeFileChooserRequest,
|
|
2606
|
+
results
|
|
2607
|
+
)
|
|
2608
|
+
) {
|
|
2609
|
+
request.tempCameraUri = null;
|
|
2610
|
+
clearActiveFileChooserRequest(request);
|
|
2589
2611
|
}
|
|
2590
|
-
mFilePathCallback = null;
|
|
2591
|
-
tempCameraUri = null;
|
|
2592
2612
|
}
|
|
2593
2613
|
)
|
|
2594
2614
|
.launch(takePictureIntent);
|
|
@@ -2598,26 +2618,27 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2598
2618
|
}
|
|
2599
2619
|
} catch (SecurityException e) {
|
|
2600
2620
|
Log.e("InAppBrowser", "Security exception launching camera: " + e.getMessage(), e);
|
|
2601
|
-
fallbackToFilePicker();
|
|
2621
|
+
fallbackToFilePicker(request);
|
|
2602
2622
|
}
|
|
2603
2623
|
} else {
|
|
2604
2624
|
Log.e("InAppBrowser", "Failed to create photo URI, falling back to file picker");
|
|
2605
|
-
fallbackToFilePicker();
|
|
2625
|
+
fallbackToFilePicker(request);
|
|
2606
2626
|
}
|
|
2607
2627
|
}
|
|
2608
2628
|
} catch (Exception e) {
|
|
2609
2629
|
Log.e("InAppBrowser", "Camera launch failed: " + e.getMessage(), e);
|
|
2610
|
-
fallbackToFilePicker();
|
|
2630
|
+
fallbackToFilePicker(request);
|
|
2611
2631
|
}
|
|
2612
2632
|
}
|
|
2613
2633
|
|
|
2614
2634
|
/**
|
|
2615
2635
|
* Fall back to file picker when camera launch fails
|
|
2616
2636
|
*/
|
|
2617
|
-
private void fallbackToFilePicker() {
|
|
2618
|
-
if (
|
|
2619
|
-
|
|
2637
|
+
private void fallbackToFilePicker(FileChooserRequestSupport.FileChooserRequest request) {
|
|
2638
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
2639
|
+
return;
|
|
2620
2640
|
}
|
|
2641
|
+
openFileChooser(request);
|
|
2621
2642
|
}
|
|
2622
2643
|
|
|
2623
2644
|
// Grant permissions for cam
|
|
@@ -4065,45 +4086,45 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
4065
4086
|
}
|
|
4066
4087
|
}
|
|
4067
4088
|
|
|
4068
|
-
private void
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4089
|
+
private void beginFileChooserRequest(ValueCallback<Uri[]> filePathCallback, String[] acceptTypes, boolean isMultiple) {
|
|
4090
|
+
if (activeFileChooserRequest != null) {
|
|
4091
|
+
FileChooserRequestSupport.cancel(activeFileChooserRequest);
|
|
4092
|
+
}
|
|
4093
|
+
activeFileChooserRequest = new FileChooserRequestSupport.FileChooserRequest(filePathCallback, acceptTypes, isMultiple);
|
|
4094
|
+
syncFileChooserPublicFields();
|
|
4095
|
+
}
|
|
4072
4096
|
|
|
4073
|
-
|
|
4074
|
-
if (
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
// Handle common web input accept types
|
|
4078
|
-
if (acceptType.equals("image/*")) {
|
|
4079
|
-
// Keep as is - image/*
|
|
4080
|
-
} else if (acceptType.contains("image/")) {
|
|
4081
|
-
// Specific image type requested but keep it general for better compatibility
|
|
4082
|
-
acceptType = "image/*";
|
|
4083
|
-
} else if (acceptType.equals("audio/*") || acceptType.contains("audio/")) {
|
|
4084
|
-
acceptType = "audio/*";
|
|
4085
|
-
} else if (acceptType.equals("video/*") || acceptType.contains("video/")) {
|
|
4086
|
-
acceptType = "video/*";
|
|
4087
|
-
} else if (acceptType.startsWith(".") || acceptType.contains(",")) {
|
|
4088
|
-
// Handle file extensions like ".pdf, .docx" by using a general mime type
|
|
4089
|
-
if (acceptType.contains(".pdf")) {
|
|
4090
|
-
acceptType = "application/pdf";
|
|
4091
|
-
} else if (acceptType.contains(".doc") || acceptType.contains(".docx")) {
|
|
4092
|
-
acceptType = "application/msword";
|
|
4093
|
-
} else if (acceptType.contains(".xls") || acceptType.contains(".xlsx")) {
|
|
4094
|
-
acceptType = "application/vnd.ms-excel";
|
|
4095
|
-
} else if (acceptType.contains(".txt") || acceptType.contains(".text")) {
|
|
4096
|
-
acceptType = "text/plain";
|
|
4097
|
-
} else {
|
|
4098
|
-
// Default for extension lists
|
|
4099
|
-
acceptType = "*/*";
|
|
4100
|
-
}
|
|
4101
|
-
}
|
|
4097
|
+
private void clearActiveFileChooserRequest(FileChooserRequestSupport.FileChooserRequest request) {
|
|
4098
|
+
if (activeFileChooserRequest == request) {
|
|
4099
|
+
activeFileChooserRequest = null;
|
|
4100
|
+
syncFileChooserPublicFields();
|
|
4102
4101
|
}
|
|
4102
|
+
}
|
|
4103
4103
|
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4104
|
+
private void syncFileChooserPublicFields() {
|
|
4105
|
+
mFilePathCallback = activeFileChooserRequest != null ? activeFileChooserRequest.callback : null;
|
|
4106
|
+
tempCameraUri = activeFileChooserRequest != null ? activeFileChooserRequest.tempCameraUri : null;
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
void completeLegacyFileChooserResult(Uri[] results) {
|
|
4110
|
+
FileChooserRequestSupport.FileChooserRequest request = activeFileChooserRequest;
|
|
4111
|
+
if (request == null) {
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
if (FileChooserRequestSupport.completeIfActive(request, activeFileChooserRequest, results)) {
|
|
4115
|
+
request.tempCameraUri = null;
|
|
4116
|
+
clearActiveFileChooserRequest(request);
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
private void openFileChooser(FileChooserRequestSupport.FileChooserRequest request) {
|
|
4121
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
4122
|
+
return;
|
|
4123
|
+
}
|
|
4124
|
+
java.util.LinkedHashSet<String> mimeTypes = FileChooserAcceptSupport.normalizeAcceptTypes(request.acceptTypes);
|
|
4125
|
+
Intent intent = FileChooserAcceptSupport.createFileChooserIntent(request.acceptTypes, request.multiple);
|
|
4126
|
+
|
|
4127
|
+
Log.d("InAppBrowser", "File picker using action: " + intent.getAction() + ", MIME types: " + mimeTypes);
|
|
4107
4128
|
|
|
4108
4129
|
try {
|
|
4109
4130
|
if (activity instanceof androidx.activity.ComponentActivity) {
|
|
@@ -4114,26 +4135,29 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
4114
4135
|
"file_chooser",
|
|
4115
4136
|
new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
|
|
4116
4137
|
(result) -> {
|
|
4138
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
Uri[] results = null;
|
|
4117
4142
|
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
4118
4143
|
Intent data = result.getData();
|
|
4119
4144
|
if (data != null) {
|
|
4120
4145
|
if (data.getClipData() != null) {
|
|
4121
4146
|
// Handle multiple files
|
|
4122
4147
|
int count = data.getClipData().getItemCount();
|
|
4123
|
-
|
|
4148
|
+
results = new Uri[count];
|
|
4124
4149
|
for (int i = 0; i < count; i++) {
|
|
4125
4150
|
results[i] = data.getClipData().getItemAt(i).getUri();
|
|
4126
4151
|
}
|
|
4127
|
-
mFilePathCallback.onReceiveValue(results);
|
|
4128
4152
|
} else if (data.getData() != null) {
|
|
4129
4153
|
// Handle single file
|
|
4130
|
-
|
|
4154
|
+
results = new Uri[] { data.getData() };
|
|
4131
4155
|
}
|
|
4132
4156
|
}
|
|
4133
|
-
} else {
|
|
4134
|
-
mFilePathCallback.onReceiveValue(null);
|
|
4135
4157
|
}
|
|
4136
|
-
|
|
4158
|
+
if (FileChooserRequestSupport.completeIfActive(request, activeFileChooserRequest, results)) {
|
|
4159
|
+
clearActiveFileChooserRequest(request);
|
|
4160
|
+
}
|
|
4137
4161
|
}
|
|
4138
4162
|
)
|
|
4139
4163
|
.launch(Intent.createChooser(intent, "Select File"));
|
|
@@ -4143,8 +4167,9 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
4143
4167
|
}
|
|
4144
4168
|
} catch (ActivityNotFoundException e) {
|
|
4145
4169
|
// If no app can handle the specific MIME type, try with a more generic one
|
|
4146
|
-
Log.e("InAppBrowser", "No app available for
|
|
4170
|
+
Log.e("InAppBrowser", "No app available for types: " + mimeTypes + ", trying with */*");
|
|
4147
4171
|
intent.setType("*/*");
|
|
4172
|
+
intent.removeExtra(Intent.EXTRA_MIME_TYPES);
|
|
4148
4173
|
try {
|
|
4149
4174
|
if (activity instanceof androidx.activity.ComponentActivity) {
|
|
4150
4175
|
androidx.activity.ComponentActivity componentActivity = (androidx.activity.ComponentActivity) activity;
|
|
@@ -4154,26 +4179,29 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
4154
4179
|
"file_chooser",
|
|
4155
4180
|
new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
|
|
4156
4181
|
(result) -> {
|
|
4182
|
+
if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
Uri[] results = null;
|
|
4157
4186
|
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
4158
4187
|
Intent data = result.getData();
|
|
4159
4188
|
if (data != null) {
|
|
4160
4189
|
if (data.getClipData() != null) {
|
|
4161
4190
|
// Handle multiple files
|
|
4162
4191
|
int count = data.getClipData().getItemCount();
|
|
4163
|
-
|
|
4192
|
+
results = new Uri[count];
|
|
4164
4193
|
for (int i = 0; i < count; i++) {
|
|
4165
4194
|
results[i] = data.getClipData().getItemAt(i).getUri();
|
|
4166
4195
|
}
|
|
4167
|
-
mFilePathCallback.onReceiveValue(results);
|
|
4168
4196
|
} else if (data.getData() != null) {
|
|
4169
4197
|
// Handle single file
|
|
4170
|
-
|
|
4198
|
+
results = new Uri[] { data.getData() };
|
|
4171
4199
|
}
|
|
4172
4200
|
}
|
|
4173
|
-
} else {
|
|
4174
|
-
mFilePathCallback.onReceiveValue(null);
|
|
4175
4201
|
}
|
|
4176
|
-
|
|
4202
|
+
if (FileChooserRequestSupport.completeIfActive(request, activeFileChooserRequest, results)) {
|
|
4203
|
+
clearActiveFileChooserRequest(request);
|
|
4204
|
+
}
|
|
4177
4205
|
}
|
|
4178
4206
|
)
|
|
4179
4207
|
.launch(Intent.createChooser(intent, "Select File"));
|
|
@@ -4184,9 +4212,8 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
4184
4212
|
} catch (ActivityNotFoundException ex) {
|
|
4185
4213
|
// If still failing, report error
|
|
4186
4214
|
Log.e("InAppBrowser", "No app can handle file picker", ex);
|
|
4187
|
-
if (
|
|
4188
|
-
|
|
4189
|
-
mFilePathCallback = null;
|
|
4215
|
+
if (FileChooserRequestSupport.cancelIfActive(request, activeFileChooserRequest)) {
|
|
4216
|
+
clearActiveFileChooserRequest(request);
|
|
4190
4217
|
}
|
|
4191
4218
|
}
|
|
4192
4219
|
}
|
|
@@ -6258,11 +6285,11 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
6258
6285
|
}
|
|
6259
6286
|
|
|
6260
6287
|
// Clear any pending callbacks to prevent memory leaks
|
|
6261
|
-
if (
|
|
6262
|
-
|
|
6263
|
-
|
|
6288
|
+
if (activeFileChooserRequest != null) {
|
|
6289
|
+
FileChooserRequestSupport.cancel(activeFileChooserRequest);
|
|
6290
|
+
activeFileChooserRequest = null;
|
|
6291
|
+
syncFileChooserPublicFields();
|
|
6264
6292
|
}
|
|
6265
|
-
tempCameraUri = null;
|
|
6266
6293
|
|
|
6267
6294
|
// Clear file inputs for security/privacy before destroying WebView
|
|
6268
6295
|
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.
|
|
418
|
+
private let pluginVersion: String = "8.15.2"
|
|
419
419
|
public let identifier = "CapgoInAppBrowserPlugin"
|
|
420
420
|
public let jsName = "CapgoInAppBrowser"
|
|
421
421
|
public let pluginMethods: [CAPPluginMethod] = [
|