@capgo/inappbrowser 6.13.2 → 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.
- package/README.md +20 -21
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/InAppBrowserPlugin.java +118 -80
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +9 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +739 -37
- package/dist/docs.json +54 -62
- package/dist/esm/definitions.d.ts +12 -13
- package/dist/esm/definitions.js.map +1 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/InAppBrowserPlugin.swift +38 -22
- package/ios/Plugin/WKWebViewController.swift +16 -107
- package/package.json +1 -1
- package/ios/Plugin/Assets.xcassets/Contents.json +0 -6
|
@@ -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
|
|
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('
|
|
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.
|
|
600
|
-
|
|
601
|
-
|
|
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.
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
"
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
}
|