@capgo/inappbrowser 6.13.1 → 6.14.0

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.
@@ -18,11 +18,15 @@ import android.graphics.PorterDuff;
18
18
  import android.graphics.PorterDuffColorFilter;
19
19
  import android.net.Uri;
20
20
  import android.net.http.SslError;
21
+ import android.os.Build;
22
+ import android.os.Environment;
23
+ import android.provider.MediaStore;
21
24
  import android.text.TextUtils;
22
25
  import android.util.Base64;
23
26
  import android.util.Log;
24
27
  import android.util.TypedValue;
25
28
  import android.view.View;
29
+ import android.view.ViewGroup;
26
30
  import android.view.Window;
27
31
  import android.view.WindowManager;
28
32
  import android.webkit.HttpAuthHandler;
@@ -41,11 +45,18 @@ import android.widget.ImageView;
41
45
  import android.widget.TextView;
42
46
  import android.widget.Toast;
43
47
  import android.widget.Toolbar;
48
+ import androidx.activity.result.ActivityResult;
49
+ import androidx.activity.result.contract.ActivityResultContracts;
50
+ import androidx.core.content.FileProvider;
51
+ import androidx.core.graphics.Insets;
52
+ import androidx.core.view.ViewCompat;
53
+ import androidx.core.view.WindowInsetsCompat;
44
54
  import androidx.core.view.WindowInsetsControllerCompat;
45
55
  import com.caverock.androidsvg.SVG;
46
56
  import com.caverock.androidsvg.SVGParseException;
47
57
  import com.getcapacitor.JSObject;
48
58
  import java.io.ByteArrayInputStream;
59
+ import java.io.File;
49
60
  import java.io.IOException;
50
61
  import java.io.InputStream;
51
62
  import java.net.URI;
@@ -105,6 +116,9 @@ public class WebViewDialog extends Dialog {
105
116
  public ValueCallback<Uri> mUploadMessage;
106
117
  public ValueCallback<Uri[]> mFilePathCallback;
107
118
 
119
+ // Temporary URI for storing camera capture
120
+ public Uri tempCameraUri;
121
+
108
122
  public interface PermissionHandler {
109
123
  void handleCameraPermissionRequest(PermissionRequest request);
110
124
 
@@ -291,6 +305,10 @@ public class WebViewDialog extends Dialog {
291
305
  );
292
306
 
293
307
  this._webView = findViewById(R.id.browser_view);
308
+
309
+ // Apply insets to fix edge-to-edge issues on Android 15+
310
+ applyInsets();
311
+
294
312
  _webView.addJavascriptInterface(
295
313
  new JavaScriptInterface(),
296
314
  "AndroidInterface"
@@ -327,21 +345,206 @@ public class WebViewDialog extends Dialog {
327
345
  FileChooserParams fileChooserParams
328
346
  ) {
329
347
  // Get the accept type safely
330
- String acceptType = "*/*"; // Default to all file types
348
+ String acceptType;
331
349
  if (
332
350
  fileChooserParams.getAcceptTypes() != null &&
333
351
  fileChooserParams.getAcceptTypes().length > 0 &&
334
352
  !TextUtils.isEmpty(fileChooserParams.getAcceptTypes()[0])
335
353
  ) {
336
354
  acceptType = fileChooserParams.getAcceptTypes()[0];
355
+ } else {
356
+ acceptType = "*/*";
337
357
  }
338
358
 
359
+ // DEBUG: Log details about the file chooser request
360
+ Log.d("InAppBrowser", "onShowFileChooser called");
361
+ Log.d("InAppBrowser", "Accept type: " + acceptType);
362
+ Log.d(
363
+ "InAppBrowser",
364
+ "Current URL: " +
365
+ (webView.getUrl() != null ? webView.getUrl() : "null")
366
+ );
367
+ Log.d(
368
+ "InAppBrowser",
369
+ "Original URL: " +
370
+ (webView.getOriginalUrl() != null
371
+ ? webView.getOriginalUrl()
372
+ : "null")
373
+ );
374
+ Log.d(
375
+ "InAppBrowser",
376
+ "Has camera permission: " +
377
+ (activity != null &&
378
+ activity.checkSelfPermission(
379
+ android.Manifest.permission.CAMERA
380
+ ) ==
381
+ android.content.pm.PackageManager.PERMISSION_GRANTED)
382
+ );
383
+
339
384
  // Check if the file chooser is already open
340
385
  if (mFilePathCallback != null) {
341
386
  mFilePathCallback.onReceiveValue(null);
342
387
  mFilePathCallback = null;
343
388
  }
344
389
 
390
+ mFilePathCallback = filePathCallback;
391
+
392
+ // Direct check for capture attribute in URL (fallback method)
393
+ boolean isCaptureInUrl;
394
+ String captureMode;
395
+ String currentUrl = webView.getUrl();
396
+
397
+ // Look for capture in URL parameters - sometimes the attribute shows up in URL
398
+ if (currentUrl != null && currentUrl.contains("capture=")) {
399
+ isCaptureInUrl = true;
400
+ captureMode = currentUrl.contains("capture=user")
401
+ ? "user"
402
+ : "environment";
403
+ Log.d("InAppBrowser", "Found capture in URL: " + captureMode);
404
+ } else {
405
+ captureMode = null;
406
+ isCaptureInUrl = false;
407
+ }
408
+
409
+ // For image inputs, try to detect capture attribute using JavaScript
410
+ if (acceptType.equals("image/*")) {
411
+ // Check if HTML content contains capture attribute on file inputs (synchronous check)
412
+ webView.evaluateJavascript(
413
+ "document.querySelector('input[type=\"file\"][capture]') !== null",
414
+ hasCaptureValue -> {
415
+ Log.d(
416
+ "InAppBrowser",
417
+ "Quick capture check: " + hasCaptureValue
418
+ );
419
+ if (Boolean.parseBoolean(hasCaptureValue.replace("\"", ""))) {
420
+ Log.d(
421
+ "InAppBrowser",
422
+ "Found capture attribute in quick check"
423
+ );
424
+ }
425
+ }
426
+ );
427
+
428
+ // Fixed JavaScript with proper error handling
429
+ String js =
430
+ "try {" +
431
+ " (function() {" +
432
+ " var captureAttr = null;" +
433
+ " // Check active element first" +
434
+ " if (document.activeElement && " +
435
+ " document.activeElement.tagName === 'INPUT' && " +
436
+ " document.activeElement.type === 'file') {" +
437
+ " if (document.activeElement.hasAttribute('capture')) {" +
438
+ " captureAttr = document.activeElement.getAttribute('capture') || 'environment';" +
439
+ " return captureAttr;" +
440
+ " }" +
441
+ " }" +
442
+ " // Try to find any input with capture attribute" +
443
+ " var inputs = document.querySelectorAll('input[type=\"file\"][capture]');" +
444
+ " if (inputs && inputs.length > 0) {" +
445
+ " captureAttr = inputs[0].getAttribute('capture') || 'environment';" +
446
+ " return captureAttr;" +
447
+ " }" +
448
+ " // Try to extract from HTML attributes" +
449
+ " var allInputs = document.getElementsByTagName('input');" +
450
+ " for (var i = 0; i < allInputs.length; i++) {" +
451
+ " var input = allInputs[i];" +
452
+ " if (input.type === 'file') {" +
453
+ " if (input.hasAttribute('capture')) {" +
454
+ " captureAttr = input.getAttribute('capture') || 'environment';" +
455
+ " return captureAttr;" +
456
+ " }" +
457
+ " // Look for the accept attribute containing image/* as this might be a camera input" +
458
+ " var acceptAttr = input.getAttribute('accept');" +
459
+ " if (acceptAttr && acceptAttr.indexOf('image/*') >= 0) {" +
460
+ " console.log('Found input with image/* accept');" +
461
+ " }" +
462
+ " }" +
463
+ " }" +
464
+ " return '';" +
465
+ " })();" +
466
+ "} catch(e) { console.error('Capture detection error:', e); return ''; }";
467
+
468
+ webView.evaluateJavascript(js, value -> {
469
+ Log.d("InAppBrowser", "Capture attribute JS result: " + value);
470
+
471
+ // If we already found capture in URL, use that directly
472
+ if (isCaptureInUrl) {
473
+ Log.d("InAppBrowser", "Using capture from URL: " + captureMode);
474
+ launchCamera(captureMode.equals("user"));
475
+ return;
476
+ }
477
+
478
+ // Process JavaScript result
479
+ if (value != null && value.length() > 2) {
480
+ // Clean up the value (remove quotes)
481
+ String captureValue = value.replace("\"", "");
482
+ Log.d(
483
+ "InAppBrowser",
484
+ "Found capture attribute: " + captureValue
485
+ );
486
+
487
+ if (!captureValue.isEmpty()) {
488
+ activity.runOnUiThread(() ->
489
+ launchCamera(captureValue.equals("user"))
490
+ );
491
+ return;
492
+ }
493
+ }
494
+
495
+ // Look for hints in the web page source
496
+ Log.d("InAppBrowser", "Looking for camera hints in page content");
497
+ webView.evaluateJavascript(
498
+ "(function() { return document.documentElement.innerHTML; })()",
499
+ htmlSource -> {
500
+ if (htmlSource != null && htmlSource.length() > 10) {
501
+ boolean hasCameraOrSelfieKeyword =
502
+ htmlSource.contains("capture=") ||
503
+ htmlSource.contains("camera") ||
504
+ htmlSource.contains("selfie");
505
+
506
+ Log.d(
507
+ "InAppBrowser",
508
+ "Page contains camera keywords: " +
509
+ hasCameraOrSelfieKeyword
510
+ );
511
+
512
+ if (
513
+ hasCameraOrSelfieKeyword &&
514
+ currentUrl != null &&
515
+ (currentUrl.contains("selfie") ||
516
+ currentUrl.contains("camera") ||
517
+ currentUrl.contains("photo"))
518
+ ) {
519
+ Log.d(
520
+ "InAppBrowser",
521
+ "URL suggests camera usage, launching camera"
522
+ );
523
+ activity.runOnUiThread(() ->
524
+ launchCamera(currentUrl.contains("selfie"))
525
+ );
526
+ return;
527
+ }
528
+ }
529
+
530
+ // If all detection methods fail, fall back to regular file picker
531
+ Log.d(
532
+ "InAppBrowser",
533
+ "No capture attribute detected, using file picker"
534
+ );
535
+ openFileChooser(
536
+ filePathCallback,
537
+ acceptType,
538
+ fileChooserParams.getMode() ==
539
+ FileChooserParams.MODE_OPEN_MULTIPLE
540
+ );
541
+ }
542
+ );
543
+ });
544
+ return true;
545
+ }
546
+
547
+ // For non-image types, use regular file picker
345
548
  openFileChooser(
346
549
  filePathCallback,
347
550
  acceptType,
@@ -350,6 +553,160 @@ public class WebViewDialog extends Dialog {
350
553
  return true;
351
554
  }
352
555
 
556
+ /**
557
+ * Launch the camera app for capturing images
558
+ * @param useFrontCamera true to use front camera, false for back camera
559
+ */
560
+ private void launchCamera(boolean useFrontCamera) {
561
+ Log.d(
562
+ "InAppBrowser",
563
+ "Launching camera, front camera: " + useFrontCamera
564
+ );
565
+
566
+ // First check if we have camera permission
567
+ if (activity != null && permissionHandler != null) {
568
+ // Create a temporary permission request to check camera permission
569
+ android.webkit.PermissionRequest tempRequest =
570
+ new android.webkit.PermissionRequest() {
571
+ @Override
572
+ public Uri getOrigin() {
573
+ return Uri.parse("file:///android_asset/");
574
+ }
575
+
576
+ @Override
577
+ public String[] getResources() {
578
+ return new String[] {
579
+ PermissionRequest.RESOURCE_VIDEO_CAPTURE,
580
+ };
581
+ }
582
+
583
+ @Override
584
+ public void grant(String[] resources) {
585
+ // Permission granted, now launch the camera
586
+ launchCameraWithPermission(useFrontCamera);
587
+ }
588
+
589
+ @Override
590
+ public void deny() {
591
+ // Permission denied, fall back to file picker
592
+ Log.e(
593
+ "InAppBrowser",
594
+ "Camera permission denied, falling back to file picker"
595
+ );
596
+ fallbackToFilePicker();
597
+ }
598
+ };
599
+
600
+ // Request camera permission through the plugin
601
+ permissionHandler.handleCameraPermissionRequest(tempRequest);
602
+ return;
603
+ }
604
+
605
+ // If we can't request permission, try launching directly
606
+ launchCameraWithPermission(useFrontCamera);
607
+ }
608
+
609
+ /**
610
+ * Launch camera after permission is granted
611
+ */
612
+ private void launchCameraWithPermission(boolean useFrontCamera) {
613
+ try {
614
+ Intent takePictureIntent = new Intent(
615
+ MediaStore.ACTION_IMAGE_CAPTURE
616
+ );
617
+ if (
618
+ takePictureIntent.resolveActivity(activity.getPackageManager()) !=
619
+ null
620
+ ) {
621
+ File photoFile = null;
622
+ try {
623
+ photoFile = createImageFile();
624
+ } catch (IOException ex) {
625
+ Log.e("InAppBrowser", "Error creating image file", ex);
626
+ fallbackToFilePicker();
627
+ return;
628
+ }
629
+
630
+ if (photoFile != null) {
631
+ tempCameraUri = FileProvider.getUriForFile(
632
+ activity,
633
+ activity.getPackageName() + ".fileprovider",
634
+ photoFile
635
+ );
636
+ takePictureIntent.putExtra(
637
+ MediaStore.EXTRA_OUTPUT,
638
+ tempCameraUri
639
+ );
640
+
641
+ if (useFrontCamera) {
642
+ takePictureIntent.putExtra(
643
+ "android.intent.extras.CAMERA_FACING",
644
+ 1
645
+ );
646
+ }
647
+
648
+ try {
649
+ if (activity instanceof androidx.activity.ComponentActivity) {
650
+ androidx.activity.ComponentActivity componentActivity =
651
+ (androidx.activity.ComponentActivity) activity;
652
+ componentActivity
653
+ .getActivityResultRegistry()
654
+ .register(
655
+ "camera_capture",
656
+ new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
657
+ result -> {
658
+ if (result.getResultCode() == Activity.RESULT_OK) {
659
+ if (tempCameraUri != null) {
660
+ mFilePathCallback.onReceiveValue(
661
+ new Uri[] { tempCameraUri }
662
+ );
663
+ }
664
+ } else {
665
+ mFilePathCallback.onReceiveValue(null);
666
+ }
667
+ mFilePathCallback = null;
668
+ tempCameraUri = null;
669
+ }
670
+ )
671
+ .launch(takePictureIntent);
672
+ } else {
673
+ // Fallback for non-ComponentActivity
674
+ activity.startActivityForResult(
675
+ takePictureIntent,
676
+ FILE_CHOOSER_REQUEST_CODE
677
+ );
678
+ }
679
+ } catch (SecurityException e) {
680
+ Log.e(
681
+ "InAppBrowser",
682
+ "Security exception launching camera: " + e.getMessage(),
683
+ e
684
+ );
685
+ fallbackToFilePicker();
686
+ }
687
+ } else {
688
+ Log.e(
689
+ "InAppBrowser",
690
+ "Failed to create photo URI, falling back to file picker"
691
+ );
692
+ fallbackToFilePicker();
693
+ }
694
+ }
695
+ } catch (Exception e) {
696
+ Log.e("InAppBrowser", "Camera launch failed: " + e.getMessage(), e);
697
+ fallbackToFilePicker();
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Fall back to file picker when camera launch fails
703
+ */
704
+ private void fallbackToFilePicker() {
705
+ if (mFilePathCallback != null) {
706
+ openFileChooser(mFilePathCallback, "image/*", false);
707
+ }
708
+ }
709
+
353
710
  // Grant permissions for cam
354
711
  @Override
355
712
  public void onPermissionRequest(final PermissionRequest request) {
@@ -445,6 +802,202 @@ public class WebViewDialog extends Dialog {
445
802
  }
446
803
  }
447
804
 
805
+ /**
806
+ * Apply window insets to the WebView to properly handle edge-to-edge display
807
+ * and fix status bar overlap issues on Android 15+
808
+ */
809
+ private void applyInsets() {
810
+ if (_webView == null) {
811
+ return;
812
+ }
813
+
814
+ // Check if we need Android 15+ specific fixes
815
+ boolean isAndroid15Plus = Build.VERSION.SDK_INT >= 35;
816
+
817
+ // Get parent view
818
+ ViewGroup parent = (ViewGroup) _webView.getParent();
819
+
820
+ // Find status bar color view and toolbar for Android 15+ specific handling
821
+ View statusBarColorView = findViewById(R.id.status_bar_color_view);
822
+ View toolbarView = findViewById(R.id.tool_bar);
823
+
824
+ // Special handling for Android 15+
825
+ if (isAndroid15Plus) {
826
+ // Get AppBarLayout which contains the toolbar
827
+ if (
828
+ toolbarView != null &&
829
+ toolbarView.getParent() instanceof
830
+ com.google.android.material.appbar.AppBarLayout appBarLayout
831
+ ) {
832
+ // Remove elevation to eliminate shadows (only on Android 15+)
833
+ appBarLayout.setElevation(0);
834
+ appBarLayout.setStateListAnimator(null);
835
+ appBarLayout.setOutlineProvider(null);
836
+
837
+ // Determine background color to use
838
+ int backgroundColor = Color.BLACK; // Default fallback
839
+ if (
840
+ _options.getToolbarColor() != null &&
841
+ !_options.getToolbarColor().isEmpty()
842
+ ) {
843
+ try {
844
+ backgroundColor = Color.parseColor(_options.getToolbarColor());
845
+ } catch (IllegalArgumentException e) {
846
+ Log.e(
847
+ "InAppBrowser",
848
+ "Invalid toolbar color, using black: " + e.getMessage()
849
+ );
850
+ }
851
+ } else {
852
+ // Follow system theme if no color specified
853
+ boolean isDarkTheme = isDarkThemeEnabled();
854
+ backgroundColor = isDarkTheme ? Color.BLACK : Color.WHITE;
855
+ }
856
+
857
+ // Apply fixes for Android 15+ using a delayed post
858
+ final int finalBgColor = backgroundColor;
859
+ _webView.post(() -> {
860
+ // Get status bar height
861
+ int statusBarHeight = 0;
862
+ int resourceId = getContext()
863
+ .getResources()
864
+ .getIdentifier("status_bar_height", "dimen", "android");
865
+ if (resourceId > 0) {
866
+ statusBarHeight = getContext()
867
+ .getResources()
868
+ .getDimensionPixelSize(resourceId);
869
+ }
870
+
871
+ // Fix status bar view
872
+ if (statusBarColorView != null) {
873
+ ViewGroup.LayoutParams params =
874
+ statusBarColorView.getLayoutParams();
875
+ params.height = statusBarHeight;
876
+ statusBarColorView.setLayoutParams(params);
877
+ statusBarColorView.setBackgroundColor(finalBgColor);
878
+ statusBarColorView.setVisibility(View.VISIBLE);
879
+ }
880
+
881
+ // Fix AppBarLayout position
882
+ ViewGroup.MarginLayoutParams params =
883
+ (ViewGroup.MarginLayoutParams) appBarLayout.getLayoutParams();
884
+ params.topMargin = statusBarHeight;
885
+ appBarLayout.setLayoutParams(params);
886
+ appBarLayout.setBackgroundColor(finalBgColor);
887
+ });
888
+ }
889
+ }
890
+
891
+ // Apply system insets to WebView (compatible with all Android versions)
892
+ ViewCompat.setOnApplyWindowInsetsListener(_webView, (v, windowInsets) -> {
893
+ Insets insets = windowInsets.getInsets(
894
+ WindowInsetsCompat.Type.systemBars()
895
+ );
896
+ Boolean keyboardVisible = windowInsets.isVisible(
897
+ WindowInsetsCompat.Type.ime()
898
+ );
899
+
900
+ ViewGroup.MarginLayoutParams mlp =
901
+ (ViewGroup.MarginLayoutParams) v.getLayoutParams();
902
+
903
+ // Apply margins based on Android version
904
+ if (isAndroid15Plus) {
905
+ // Android 15+ specific handling
906
+ if (keyboardVisible) {
907
+ mlp.bottomMargin = 0;
908
+ } else {
909
+ mlp.bottomMargin = insets.bottom;
910
+ }
911
+ // On Android 15+, don't add top margin as it's handled by AppBarLayout
912
+ mlp.topMargin = 0;
913
+ } else {
914
+ // Original behavior for older Android versions
915
+ mlp.topMargin = insets.top;
916
+ mlp.bottomMargin = insets.bottom;
917
+ }
918
+
919
+ // These stay the same for all Android versions
920
+ mlp.leftMargin = insets.left;
921
+ mlp.rightMargin = insets.right;
922
+ v.setLayoutParams(mlp);
923
+
924
+ return WindowInsetsCompat.CONSUMED;
925
+ });
926
+
927
+ // Handle window decoration - version-specific window settings
928
+ if (getWindow() != null) {
929
+ if (isAndroid15Plus) {
930
+ // Only for Android 15+: Set window to draw behind status bar
931
+ getWindow().setDecorFitsSystemWindows(false);
932
+ getWindow().setStatusBarColor(Color.TRANSPARENT);
933
+
934
+ // Set status bar text color
935
+ int backgroundColor;
936
+ if (
937
+ _options.getToolbarColor() != null &&
938
+ !_options.getToolbarColor().isEmpty()
939
+ ) {
940
+ try {
941
+ backgroundColor = Color.parseColor(_options.getToolbarColor());
942
+ boolean isDarkBackground = isDarkColor(backgroundColor);
943
+ WindowInsetsControllerCompat controller =
944
+ new WindowInsetsControllerCompat(
945
+ getWindow(),
946
+ getWindow().getDecorView()
947
+ );
948
+ controller.setAppearanceLightStatusBars(!isDarkBackground);
949
+ } catch (IllegalArgumentException e) {
950
+ // Ignore color parsing errors
951
+ }
952
+ }
953
+ } else if (Build.VERSION.SDK_INT >= 30) {
954
+ // Android 11-14: Use original behavior
955
+ WindowInsetsControllerCompat controller =
956
+ new WindowInsetsControllerCompat(
957
+ getWindow(),
958
+ getWindow().getDecorView()
959
+ );
960
+
961
+ // Original behavior for status bar color
962
+ if (
963
+ _options.getToolbarColor() != null &&
964
+ !_options.getToolbarColor().isEmpty()
965
+ ) {
966
+ try {
967
+ int toolbarColor = Color.parseColor(_options.getToolbarColor());
968
+ getWindow().setStatusBarColor(toolbarColor);
969
+
970
+ boolean isDarkBackground = isDarkColor(toolbarColor);
971
+ controller.setAppearanceLightStatusBars(!isDarkBackground);
972
+ } catch (IllegalArgumentException e) {
973
+ // Ignore color parsing errors
974
+ }
975
+ }
976
+ } else {
977
+ // Pre-Android 11: Original behavior with deprecated flags
978
+ getWindow()
979
+ .getDecorView()
980
+ .setSystemUiVisibility(
981
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
982
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
983
+ );
984
+
985
+ // Apply original status bar color logic
986
+ if (
987
+ _options.getToolbarColor() != null &&
988
+ !_options.getToolbarColor().isEmpty()
989
+ ) {
990
+ try {
991
+ int toolbarColor = Color.parseColor(_options.getToolbarColor());
992
+ getWindow().setStatusBarColor(toolbarColor);
993
+ } catch (IllegalArgumentException e) {
994
+ // Ignore color parsing errors
995
+ }
996
+ }
997
+ }
998
+ }
999
+ }
1000
+
448
1001
  public void postMessageToJS(Object detail) {
449
1002
  if (_webView != null) {
450
1003
  try {
@@ -495,7 +1048,7 @@ public class WebViewDialog extends Dialog {
495
1048
  _options.getPreShowScript() +
496
1049
  '\n' +
497
1050
  "};\n" +
498
- "preShowFunction().then(() => window.PreShowScriptInterface.success()).catch(err => { console.error('Preshow error', err); window.PreShowScriptInterface.error(JSON.stringify(err, Object.getOwnPropertyNames(err))) })";
1051
+ "preShowFunction().then(() => window.PreShowScriptInterface.success()).catch(err => { console.error('Pre show error', err); window.PreShowScriptInterface.error(JSON.stringify(err, Object.getOwnPropertyNames(err))) })";
499
1052
 
500
1053
  Log.i(
501
1054
  "InjectPreShowScript",
@@ -596,10 +1149,47 @@ public class WebViewDialog extends Dialog {
596
1149
  intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultiple);
597
1150
 
598
1151
  try {
599
- activity.startActivityForResult(
600
- Intent.createChooser(intent, "Select File"),
601
- FILE_CHOOSER_REQUEST_CODE
602
- );
1152
+ if (activity instanceof androidx.activity.ComponentActivity) {
1153
+ androidx.activity.ComponentActivity componentActivity =
1154
+ (androidx.activity.ComponentActivity) activity;
1155
+ componentActivity
1156
+ .getActivityResultRegistry()
1157
+ .register(
1158
+ "file_chooser",
1159
+ new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
1160
+ result -> {
1161
+ if (result.getResultCode() == Activity.RESULT_OK) {
1162
+ Intent data = result.getData();
1163
+ if (data != null) {
1164
+ if (data.getClipData() != null) {
1165
+ // Handle multiple files
1166
+ int count = data.getClipData().getItemCount();
1167
+ Uri[] results = new Uri[count];
1168
+ for (int i = 0; i < count; i++) {
1169
+ results[i] = data.getClipData().getItemAt(i).getUri();
1170
+ }
1171
+ mFilePathCallback.onReceiveValue(results);
1172
+ } else if (data.getData() != null) {
1173
+ // Handle single file
1174
+ mFilePathCallback.onReceiveValue(
1175
+ new Uri[] { data.getData() }
1176
+ );
1177
+ }
1178
+ }
1179
+ } else {
1180
+ mFilePathCallback.onReceiveValue(null);
1181
+ }
1182
+ mFilePathCallback = null;
1183
+ }
1184
+ )
1185
+ .launch(Intent.createChooser(intent, "Select File"));
1186
+ } else {
1187
+ // Fallback for non-ComponentActivity
1188
+ activity.startActivityForResult(
1189
+ Intent.createChooser(intent, "Select File"),
1190
+ FILE_CHOOSER_REQUEST_CODE
1191
+ );
1192
+ }
603
1193
  } catch (ActivityNotFoundException e) {
604
1194
  // If no app can handle the specific MIME type, try with a more generic one
605
1195
  Log.e(
@@ -608,10 +1198,47 @@ public class WebViewDialog extends Dialog {
608
1198
  );
609
1199
  intent.setType("*/*");
610
1200
  try {
611
- activity.startActivityForResult(
612
- Intent.createChooser(intent, "Select File"),
613
- FILE_CHOOSER_REQUEST_CODE
614
- );
1201
+ if (activity instanceof androidx.activity.ComponentActivity) {
1202
+ androidx.activity.ComponentActivity componentActivity =
1203
+ (androidx.activity.ComponentActivity) activity;
1204
+ componentActivity
1205
+ .getActivityResultRegistry()
1206
+ .register(
1207
+ "file_chooser",
1208
+ new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
1209
+ result -> {
1210
+ if (result.getResultCode() == Activity.RESULT_OK) {
1211
+ Intent data = result.getData();
1212
+ if (data != null) {
1213
+ if (data.getClipData() != null) {
1214
+ // Handle multiple files
1215
+ int count = data.getClipData().getItemCount();
1216
+ Uri[] results = new Uri[count];
1217
+ for (int i = 0; i < count; i++) {
1218
+ results[i] = data.getClipData().getItemAt(i).getUri();
1219
+ }
1220
+ mFilePathCallback.onReceiveValue(results);
1221
+ } else if (data.getData() != null) {
1222
+ // Handle single file
1223
+ mFilePathCallback.onReceiveValue(
1224
+ new Uri[] { data.getData() }
1225
+ );
1226
+ }
1227
+ }
1228
+ } else {
1229
+ mFilePathCallback.onReceiveValue(null);
1230
+ }
1231
+ mFilePathCallback = null;
1232
+ }
1233
+ )
1234
+ .launch(Intent.createChooser(intent, "Select File"));
1235
+ } else {
1236
+ // Fallback for non-ComponentActivity
1237
+ activity.startActivityForResult(
1238
+ Intent.createChooser(intent, "Select File"),
1239
+ FILE_CHOOSER_REQUEST_CODE
1240
+ );
1241
+ }
615
1242
  } catch (ActivityNotFoundException ex) {
616
1243
  // If still failing, report error
617
1244
  Log.e("InAppBrowser", "No app can handle file picker", ex);
@@ -983,15 +1610,6 @@ public class WebViewDialog extends Dialog {
983
1610
  }
984
1611
  }
985
1612
 
986
- if (inputStream == null) {
987
- Log.e(
988
- "InAppBrowser",
989
- "Failed to load SVG icon: " + buttonNearDone.getIcon()
990
- );
991
- buttonNearDoneView.setVisibility(View.GONE);
992
- return;
993
- }
994
-
995
1613
  // Parse and render SVG
996
1614
  SVG svg = SVG.getFromInputStream(inputStream);
997
1615
  if (svg == null) {
@@ -1260,11 +1878,16 @@ public class WebViewDialog extends Dialog {
1260
1878
  WebView view,
1261
1879
  WebResourceRequest request
1262
1880
  ) {
1263
- // HashMap<String, String> map = new HashMap<>();
1264
- // map.put("x-requested-with", null);
1265
- // view.loadUrl(request.getUrl().toString(), map);
1266
1881
  Context context = view.getContext();
1267
1882
  String url = request.getUrl().toString();
1883
+ Log.d("InAppBrowser", "shouldOverrideUrlLoading: " + url);
1884
+ // If preventDeeplink is true, don't handle any non-http(s) URLs
1885
+ if (_options.getPreventDeeplink()) {
1886
+ Log.d("InAppBrowser", "preventDeeplink is true");
1887
+ if (!url.startsWith("https://") && !url.startsWith("http://")) {
1888
+ return true;
1889
+ }
1890
+ }
1268
1891
 
1269
1892
  if (!url.startsWith("https://") && !url.startsWith("http://")) {
1270
1893
  try {
@@ -1672,6 +2295,18 @@ public class WebViewDialog extends Dialog {
1672
2295
  @Override
1673
2296
  public void dismiss() {
1674
2297
  if (_webView != null) {
2298
+ // Reset file inputs to prevent WebView from caching them
2299
+ _webView.evaluateJavascript(
2300
+ "(function() {" +
2301
+ " var inputs = document.querySelectorAll('input[type=\"file\"]');" +
2302
+ " for (var i = 0; i < inputs.length; i++) {" +
2303
+ " inputs[i].value = '';" +
2304
+ " }" +
2305
+ " return true;" +
2306
+ "})();",
2307
+ null
2308
+ );
2309
+
1675
2310
  _webView.loadUrl("about:blank");
1676
2311
  _webView.onPause();
1677
2312
  _webView.removeAllViews();
@@ -1769,25 +2404,92 @@ public class WebViewDialog extends Dialog {
1769
2404
 
1770
2405
  // This script adds minimal fixes for date inputs to use Material Design
1771
2406
  String script =
1772
- "(function() {\n" +
1773
- " // Find all date inputs\n" +
1774
- " const dateInputs = document.querySelectorAll('input[type=\"date\"]');\n" +
1775
- " dateInputs.forEach(input => {\n" +
1776
- " // Ensure change events propagate correctly\n" +
1777
- " let lastValue = input.value;\n" +
1778
- " input.addEventListener('change', () => {\n" +
1779
- " if (input.value !== lastValue) {\n" +
1780
- " lastValue = input.value;\n" +
1781
- " // Dispatch an input event to ensure frameworks detect the change\n" +
1782
- " input.dispatchEvent(new Event('input', { bubbles: true }));\n" +
1783
- " }\n" +
1784
- " });\n" +
1785
- " });\n" +
1786
- "})();";
2407
+ """
2408
+ (function() {
2409
+ // Find all date inputs
2410
+ const dateInputs = document.querySelectorAll('input[type="date"]');
2411
+ dateInputs.forEach(input => {
2412
+ // Ensure change events propagate correctly
2413
+ let lastValue = input.value;
2414
+ input.addEventListener('change', () => {
2415
+ if (input.value !== lastValue) {
2416
+ lastValue = input.value;
2417
+ // Dispatch an input event to ensure frameworks detect the change
2418
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2419
+ }
2420
+ });
2421
+ });
2422
+ })();""";
1787
2423
 
1788
2424
  // Execute the script in the WebView
1789
2425
  _webView.post(() -> _webView.evaluateJavascript(script, null));
1790
2426
 
1791
2427
  Log.d("InAppBrowser", "Applied minimal date picker fixes");
1792
2428
  }
2429
+
2430
+ /**
2431
+ * Creates a temporary URI for storing camera capture
2432
+ * @return URI for the temporary file or null if creation failed
2433
+ */
2434
+ private Uri createTempImageUri() {
2435
+ try {
2436
+ String fileName = "capture_" + System.currentTimeMillis() + ".jpg";
2437
+ java.io.File cacheDir = _context.getCacheDir();
2438
+
2439
+ // Make sure cache directory exists
2440
+ if (!cacheDir.exists() && !cacheDir.mkdirs()) {
2441
+ return null;
2442
+ }
2443
+
2444
+ // Create temporary file
2445
+ java.io.File tempFile = new java.io.File(cacheDir, fileName);
2446
+ if (!tempFile.createNewFile()) {
2447
+ return null;
2448
+ }
2449
+
2450
+ // Get content URI through FileProvider
2451
+ try {
2452
+ return androidx.core.content.FileProvider.getUriForFile(
2453
+ _context,
2454
+ _context.getPackageName() + ".fileprovider",
2455
+ tempFile
2456
+ );
2457
+ } catch (IllegalArgumentException e) {
2458
+ // Try using external storage as fallback
2459
+ java.io.File externalCacheDir = _context.getExternalCacheDir();
2460
+ if (externalCacheDir != null) {
2461
+ tempFile = new java.io.File(externalCacheDir, fileName);
2462
+ final boolean newFile = tempFile.createNewFile();
2463
+ if (!newFile) {
2464
+ Log.d("InAppBrowser", "Error creating new file");
2465
+ }
2466
+ return androidx.core.content.FileProvider.getUriForFile(
2467
+ _context,
2468
+ _context.getPackageName() + ".fileprovider",
2469
+ tempFile
2470
+ );
2471
+ }
2472
+ }
2473
+ return null;
2474
+ } catch (Exception e) {
2475
+ return null;
2476
+ }
2477
+ }
2478
+
2479
+ private File createImageFile() throws IOException {
2480
+ // Create an image file name
2481
+ String timeStamp = new java.text.SimpleDateFormat("yyyyMMdd_HHmmss").format(
2482
+ new java.util.Date()
2483
+ );
2484
+ String imageFileName = "JPEG_" + timeStamp + "_";
2485
+ File storageDir = activity.getExternalFilesDir(
2486
+ Environment.DIRECTORY_PICTURES
2487
+ );
2488
+ File image = File.createTempFile(
2489
+ imageFileName,/* prefix */
2490
+ ".jpg",/* suffix */
2491
+ storageDir/* directory */
2492
+ );
2493
+ return image;
2494
+ }
1793
2495
  }