@capgo/inappbrowser 8.0.0 → 8.0.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.
Files changed (50) hide show
  1. package/CapgoInappbrowser.podspec +2 -2
  2. package/LICENSE +373 -21
  3. package/Package.swift +28 -0
  4. package/README.md +600 -74
  5. package/android/build.gradle +17 -16
  6. package/android/src/main/AndroidManifest.xml +14 -2
  7. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/InAppBrowserPlugin.java +952 -204
  8. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +478 -81
  9. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewCallbacks.java +10 -4
  10. package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +3021 -226
  11. package/android/src/main/res/drawable/ic_refresh.xml +9 -0
  12. package/android/src/main/res/drawable/ic_share.xml +10 -0
  13. package/android/src/main/res/layout/activity_browser.xml +10 -0
  14. package/android/src/main/res/layout/content_browser.xml +3 -2
  15. package/android/src/main/res/layout/tool_bar.xml +44 -7
  16. package/android/src/main/res/values/strings.xml +4 -0
  17. package/android/src/main/res/values/themes.xml +27 -0
  18. package/android/src/main/res/xml/file_paths.xml +14 -0
  19. package/dist/docs.json +1289 -149
  20. package/dist/esm/definitions.d.ts +614 -25
  21. package/dist/esm/definitions.js +17 -1
  22. package/dist/esm/definitions.js.map +1 -1
  23. package/dist/esm/index.d.ts +2 -2
  24. package/dist/esm/index.js +4 -4
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/esm/web.d.ts +16 -3
  27. package/dist/esm/web.js +43 -7
  28. package/dist/esm/web.js.map +1 -1
  29. package/dist/plugin.cjs.js +60 -8
  30. package/dist/plugin.cjs.js.map +1 -1
  31. package/dist/plugin.js +60 -8
  32. package/dist/plugin.js.map +1 -1
  33. package/ios/{Plugin → Sources/InAppBrowserPlugin}/Enums.swift +5 -5
  34. package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +954 -0
  35. package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +2003 -0
  36. package/package.json +32 -30
  37. package/ios/Plugin/Assets.xcassets/Back.imageset/Back.png +0 -0
  38. package/ios/Plugin/Assets.xcassets/Back.imageset/Back@2x.png +0 -0
  39. package/ios/Plugin/Assets.xcassets/Back.imageset/Back@3x.png +0 -0
  40. package/ios/Plugin/Assets.xcassets/Back.imageset/Contents.json +0 -26
  41. package/ios/Plugin/Assets.xcassets/Contents.json +0 -6
  42. package/ios/Plugin/Assets.xcassets/Forward.imageset/Contents.json +0 -26
  43. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward.png +0 -0
  44. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward@2x.png +0 -0
  45. package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward@3x.png +0 -0
  46. package/ios/Plugin/InAppBrowserPlugin.h +0 -10
  47. package/ios/Plugin/InAppBrowserPlugin.m +0 -17
  48. package/ios/Plugin/InAppBrowserPlugin.swift +0 -203
  49. package/ios/Plugin/Info.plist +0 -24
  50. package/ios/Plugin/WKWebViewController.swift +0 -784
@@ -1,262 +1,3057 @@
1
1
  package ee.forgr.capacitor_inappbrowser;
2
2
 
3
+ import android.annotation.SuppressLint;
4
+ import android.app.Activity;
5
+ import android.app.AlertDialog;
3
6
  import android.app.Dialog;
7
+ import android.content.ActivityNotFoundException;
4
8
  import android.content.Context;
9
+ import android.content.DialogInterface;
10
+ import android.content.Intent;
11
+ import android.content.res.AssetManager;
12
+ import android.content.res.Resources;
5
13
  import android.graphics.Bitmap;
14
+ import android.graphics.Canvas;
15
+ import android.graphics.Color;
16
+ import android.graphics.Paint;
17
+ import android.graphics.PorterDuff;
18
+ import android.graphics.PorterDuffColorFilter;
19
+ import android.net.Uri;
20
+ import android.net.http.SslError;
21
+ import android.os.Build;
22
+ import android.os.Environment;
23
+ import android.print.PrintAttributes;
24
+ import android.print.PrintDocumentAdapter;
25
+ import android.print.PrintManager;
26
+ import android.provider.MediaStore;
27
+ import android.security.KeyChain;
28
+ import android.security.KeyChainAliasCallback;
6
29
  import android.text.TextUtils;
30
+ import android.util.Base64;
31
+ import android.util.Log;
32
+ import android.util.TypedValue;
33
+ import android.view.KeyEvent;
7
34
  import android.view.View;
35
+ import android.view.ViewGroup;
8
36
  import android.view.Window;
9
37
  import android.view.WindowManager;
38
+ import android.webkit.HttpAuthHandler;
39
+ import android.webkit.JavascriptInterface;
40
+ import android.webkit.PermissionRequest;
41
+ import android.webkit.SslErrorHandler;
42
+ import android.webkit.ValueCallback;
43
+ import android.webkit.WebChromeClient;
10
44
  import android.webkit.WebResourceError;
11
45
  import android.webkit.WebResourceRequest;
46
+ import android.webkit.WebResourceResponse;
12
47
  import android.webkit.WebView;
13
48
  import android.webkit.WebViewClient;
14
49
  import android.widget.ImageButton;
50
+ import android.widget.ImageView;
15
51
  import android.widget.TextView;
52
+ import android.widget.Toast;
16
53
  import android.widget.Toolbar;
54
+ import androidx.activity.result.ActivityResult;
55
+ import androidx.activity.result.contract.ActivityResultContracts;
56
+ import androidx.core.content.FileProvider;
57
+ import androidx.core.graphics.Insets;
58
+ import androidx.core.view.ViewCompat;
59
+ import androidx.core.view.WindowInsetsCompat;
60
+ import androidx.core.view.WindowInsetsControllerCompat;
61
+ import androidx.webkit.WebSettingsCompat;
62
+ import androidx.webkit.WebViewFeature;
63
+ import com.caverock.androidsvg.SVG;
64
+ import com.caverock.androidsvg.SVGParseException;
65
+ import com.getcapacitor.JSObject;
66
+ import java.io.ByteArrayInputStream;
67
+ import java.io.File;
68
+ import java.io.IOException;
69
+ import java.io.InputStream;
17
70
  import java.net.URI;
18
71
  import java.net.URISyntaxException;
72
+ import java.nio.charset.StandardCharsets;
73
+ import java.security.PrivateKey;
74
+ import java.security.cert.X509Certificate;
75
+ import java.util.Arrays;
19
76
  import java.util.HashMap;
20
77
  import java.util.Iterator;
78
+ import java.util.List;
21
79
  import java.util.Map;
80
+ import java.util.Objects;
81
+ import java.util.UUID;
82
+ import java.util.concurrent.ExecutorService;
83
+ import java.util.concurrent.Executors;
84
+ import java.util.concurrent.Semaphore;
85
+ import java.util.concurrent.TimeUnit;
86
+ import java.util.regex.Matcher;
87
+ import java.util.regex.Pattern;
88
+ import org.json.JSONException;
89
+ import org.json.JSONObject;
22
90
 
23
91
  public class WebViewDialog extends Dialog {
24
92
 
25
- private WebView _webView;
26
- private Toolbar _toolbar;
27
- private Options _options;
28
- private boolean isInitialized = false;
29
-
30
- public WebViewDialog(Context context, int theme, Options options) {
31
- super(context, theme);
32
- this._options = options;
33
- this.isInitialized = false;
34
- }
35
-
36
- public void presentWebView() {
37
- requestWindowFeature(Window.FEATURE_NO_TITLE);
38
- setCancelable(true);
39
- getWindow()
40
- .setFlags(
41
- WindowManager.LayoutParams.FLAG_FULLSCREEN,
42
- WindowManager.LayoutParams.FLAG_FULLSCREEN
43
- );
44
- setContentView(R.layout.activity_browser);
45
- getWindow()
46
- .setLayout(
47
- WindowManager.LayoutParams.MATCH_PARENT,
48
- WindowManager.LayoutParams.MATCH_PARENT
49
- );
50
-
51
- this._webView = findViewById(R.id.browser_view);
52
-
53
- _webView.getSettings().setJavaScriptEnabled(true);
54
- _webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
55
- _webView.getSettings().setDatabaseEnabled(true);
56
- _webView.getSettings().setDomStorageEnabled(true);
57
- _webView
58
- .getSettings()
59
- .setPluginState(android.webkit.WebSettings.PluginState.ON);
60
- _webView.getSettings().setLoadWithOverviewMode(true);
61
- _webView.getSettings().setUseWideViewPort(true);
62
-
63
- Map<String, String> requestHeaders = new HashMap<>();
64
- if (_options.getHeaders() != null) {
65
- Iterator<String> keys = _options.getHeaders().keys();
66
- while (keys.hasNext()) {
67
- String key = keys.next();
68
- if (TextUtils.equals(key, "User-Agent")) {
69
- _webView
70
- .getSettings()
71
- .setUserAgentString(_options.getHeaders().getString(key));
93
+ private static class ProxiedRequest {
94
+
95
+ private WebResourceResponse response;
96
+ private final Semaphore semaphore;
97
+
98
+ public WebResourceResponse getResponse() {
99
+ return response;
100
+ }
101
+
102
+ public ProxiedRequest() {
103
+ this.semaphore = new Semaphore(0);
104
+ this.response = null;
105
+ }
106
+ }
107
+
108
+ private WebView _webView;
109
+ private Toolbar _toolbar;
110
+ private Options _options = null;
111
+ private final Context _context;
112
+ public Activity activity;
113
+ private boolean isInitialized = false;
114
+ private boolean datePickerInjected = false; // Track if we've injected date picker fixes
115
+ private final WebView capacitorWebView;
116
+ private final Map<String, ProxiedRequest> proxiedRequestsHashmap = new HashMap<>();
117
+ private final ExecutorService executorService = Executors.newCachedThreadPool();
118
+ private int iconColor = Color.BLACK; // Default icon color
119
+
120
+ Semaphore preShowSemaphore = null;
121
+ String preShowError = null;
122
+
123
+ public PermissionRequest currentPermissionRequest;
124
+ public static final int FILE_CHOOSER_REQUEST_CODE = 1000;
125
+ public ValueCallback<Uri> mUploadMessage;
126
+ public ValueCallback<Uri[]> mFilePathCallback;
127
+
128
+ // Temporary URI for storing camera capture
129
+ public Uri tempCameraUri;
130
+
131
+ public interface PermissionHandler {
132
+ void handleCameraPermissionRequest(PermissionRequest request);
133
+
134
+ void handleMicrophonePermissionRequest(PermissionRequest request);
135
+ }
136
+
137
+ private final PermissionHandler permissionHandler;
138
+
139
+ public WebViewDialog(Context context, int theme, Options options, PermissionHandler permissionHandler, WebView capacitorWebView) {
140
+ // Use Material theme only if materialPicker is enabled
141
+ super(context, options.getMaterialPicker() ? R.style.InAppBrowserMaterialTheme : theme);
142
+ this._options = options;
143
+ this._context = context;
144
+ this.permissionHandler = permissionHandler;
145
+ this.isInitialized = false;
146
+ this.capacitorWebView = capacitorWebView;
147
+ }
148
+
149
+ // Add this class to provide safer JavaScript interface
150
+ private class JavaScriptInterface {
151
+
152
+ @JavascriptInterface
153
+ public void postMessage(String message) {
154
+ try {
155
+ // Handle message from JavaScript safely
156
+ if (message == null || message.isEmpty()) {
157
+ Log.e("InAppBrowser", "Received empty message from WebView");
158
+ return;
159
+ }
160
+
161
+ if (_options == null || _options.getCallbacks() == null) {
162
+ Log.e("InAppBrowser", "Cannot handle postMessage - options or callbacks are null");
163
+ return;
164
+ }
165
+
166
+ _options.getCallbacks().javascriptCallback(message);
167
+ } catch (Exception e) {
168
+ Log.e("InAppBrowser", "Error in postMessage: " + e.getMessage());
169
+ }
170
+ }
171
+
172
+ @JavascriptInterface
173
+ public void close() {
174
+ try {
175
+ // close webview safely
176
+ if (activity == null) {
177
+ Log.e("InAppBrowser", "Cannot close - activity is null");
178
+ return;
179
+ }
180
+
181
+ activity.runOnUiThread(() -> {
182
+ try {
183
+ String currentUrl = getUrl();
184
+ dismiss();
185
+
186
+ if (_options != null && _options.getCallbacks() != null) {
187
+ _options.getCallbacks().closeEvent(currentUrl);
188
+ }
189
+ } catch (Exception e) {
190
+ Log.e("InAppBrowser", "Error closing WebView: " + e.getMessage());
191
+ }
192
+ });
193
+ } catch (Exception e) {
194
+ Log.e("InAppBrowser", "Error in close: " + e.getMessage());
195
+ }
196
+ }
197
+ }
198
+
199
+ public class PreShowScriptInterface {
200
+
201
+ @JavascriptInterface
202
+ public void error(String error) {
203
+ try {
204
+ // Handle message from JavaScript
205
+ if (preShowSemaphore != null) {
206
+ preShowError = error;
207
+ preShowSemaphore.release();
208
+ }
209
+ } catch (Exception e) {
210
+ Log.e("InAppBrowser", "Error in error callback: " + e.getMessage());
211
+ }
212
+ }
213
+
214
+ @JavascriptInterface
215
+ public void success() {
216
+ try {
217
+ // Handle message from JavaScript
218
+ if (preShowSemaphore != null) {
219
+ preShowSemaphore.release();
220
+ }
221
+ } catch (Exception e) {
222
+ Log.e("InAppBrowser", "Error in success callback: " + e.getMessage());
223
+ }
224
+ }
225
+ }
226
+
227
+ public class PrintInterface {
228
+
229
+ private Context context;
230
+ private WebView webView;
231
+
232
+ public PrintInterface(Context context, WebView webView) {
233
+ this.context = context;
234
+ this.webView = webView;
235
+ }
236
+
237
+ @JavascriptInterface
238
+ public void print() {
239
+ // Run on UI thread since printing requires UI operations
240
+ ((Activity) context).runOnUiThread(
241
+ new Runnable() {
242
+ @Override
243
+ public void run() {
244
+ // Create a print job from the WebView content
245
+ PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
246
+ String jobName = "Document_" + System.currentTimeMillis();
247
+
248
+ PrintDocumentAdapter printAdapter;
249
+
250
+ // For API 21+ (Lollipop and above)
251
+ printAdapter = webView.createPrintDocumentAdapter(jobName);
252
+
253
+ printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build());
254
+ }
255
+ }
256
+ );
257
+ }
258
+ }
259
+
260
+ @SuppressLint({ "SetJavaScriptEnabled", "AddJavascriptInterface" })
261
+ public void presentWebView() {
262
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
263
+ setCancelable(true);
264
+ Objects.requireNonNull(getWindow()).setFlags(
265
+ WindowManager.LayoutParams.FLAG_FULLSCREEN,
266
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
267
+ );
268
+ setContentView(R.layout.activity_browser);
269
+
270
+ // If custom dimensions are set, configure for touch passthrough
271
+ if (_options != null && (_options.getWidth() != null || _options.getHeight() != null)) {
272
+ Window window = getWindow();
273
+ if (window != null) {
274
+ // Make the dialog background transparent
275
+ window.setBackgroundDrawableResource(android.R.color.transparent);
276
+ // Don't dim the background
277
+ window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
278
+ // Allow touches outside to pass through
279
+ window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
280
+ }
281
+ }
282
+
283
+ // Set fitsSystemWindows only for Android 10 (API 29)
284
+ if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.Q) {
285
+ View coordinator = findViewById(R.id.coordinator_layout);
286
+ if (coordinator != null) coordinator.setFitsSystemWindows(true);
287
+ View appBar = findViewById(R.id.app_bar_layout);
288
+ if (appBar != null) appBar.setFitsSystemWindows(true);
289
+ }
290
+
291
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
292
+
293
+ // Make status bar transparent
294
+ if (getWindow() != null) {
295
+ getWindow().setStatusBarColor(Color.TRANSPARENT);
296
+
297
+ // Add FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS flag to the window
298
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
299
+
300
+ // On Android 30+ clear FLAG_TRANSLUCENT_STATUS flag
301
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
302
+ }
303
+
304
+ WindowInsetsControllerCompat insetsController = new WindowInsetsControllerCompat(
305
+ getWindow(),
306
+ getWindow() != null ? getWindow().getDecorView() : null
307
+ );
308
+
309
+ if (getWindow() != null) {
310
+ getWindow()
311
+ .getDecorView()
312
+ .post(() -> {
313
+ // Get status bar height
314
+ int statusBarHeight = 0;
315
+ int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
316
+ if (resourceId > 0) {
317
+ statusBarHeight = getContext().getResources().getDimensionPixelSize(resourceId);
318
+ }
319
+
320
+ // Find the status bar color view
321
+ View statusBarColorView = findViewById(R.id.status_bar_color_view);
322
+
323
+ // Set the height of the status bar color view
324
+ if (statusBarColorView != null) {
325
+ statusBarColorView.getLayoutParams().height = statusBarHeight;
326
+ statusBarColorView.requestLayout();
327
+
328
+ // Set color based on toolbar color or dark mode
329
+ if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
330
+ try {
331
+ // Use explicitly provided toolbar color for status bar
332
+ int toolbarColor = Color.parseColor(_options.getToolbarColor());
333
+ statusBarColorView.setBackgroundColor(toolbarColor);
334
+
335
+ // Set status bar text to white or black based on background
336
+ boolean isDarkBackground = isDarkColor(toolbarColor);
337
+ insetsController.setAppearanceLightStatusBars(!isDarkBackground);
338
+ } catch (IllegalArgumentException e) {
339
+ // Fallback to default black if color parsing fails
340
+ statusBarColorView.setBackgroundColor(Color.BLACK);
341
+ insetsController.setAppearanceLightStatusBars(false);
342
+ }
343
+ } else {
344
+ // Follow system dark mode if no toolbar color provided
345
+ boolean isDarkTheme = isDarkThemeEnabled();
346
+ int statusBarColor = isDarkTheme ? Color.BLACK : Color.WHITE;
347
+ statusBarColorView.setBackgroundColor(statusBarColor);
348
+ insetsController.setAppearanceLightStatusBars(!isDarkTheme);
349
+ }
350
+ }
351
+ });
352
+ }
353
+
354
+ // Set dimensions if specified, otherwise fullscreen
355
+ applyDimensions();
356
+
357
+ this._webView = findViewById(R.id.browser_view);
358
+
359
+ // Apply insets to fix edge-to-edge issues on Android 15+
360
+ applyInsets();
361
+
362
+ _webView.addJavascriptInterface(new JavaScriptInterface(), "AndroidInterface");
363
+ _webView.addJavascriptInterface(new PreShowScriptInterface(), "PreShowScriptInterface");
364
+ _webView.addJavascriptInterface(new PrintInterface(this._context, _webView), "PrintInterface");
365
+ _webView.getSettings().setJavaScriptEnabled(true);
366
+ _webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
367
+ _webView.getSettings().setDatabaseEnabled(true);
368
+ _webView.getSettings().setDomStorageEnabled(true);
369
+ _webView.getSettings().setAllowFileAccess(true);
370
+ _webView.getSettings().setLoadWithOverviewMode(true);
371
+ _webView.getSettings().setUseWideViewPort(true);
372
+ _webView.getSettings().setAllowFileAccessFromFileURLs(true);
373
+ _webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
374
+ _webView.getSettings().setMediaPlaybackRequiresUserGesture(false);
375
+
376
+ // Open links in external browser for target="_blank" if preventDeepLink is false
377
+ if (!_options.getPreventDeeplink()) {
378
+ _webView.getSettings().setSupportMultipleWindows(true);
379
+ }
380
+
381
+ // Enhanced settings for Google Pay and Payment Request API support (only when enabled)
382
+ if (_options.getEnableGooglePaySupport()) {
383
+ Log.d("InAppBrowser", "Enabling Google Pay support features");
384
+ _webView.getSettings().setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
385
+ _webView.getSettings().setSupportMultipleWindows(true);
386
+ _webView.getSettings().setGeolocationEnabled(true);
387
+
388
+ // Ensure secure context for Payment Request API
389
+ _webView.getSettings().setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
390
+
391
+ // Enable Payment Request API only if feature is supported
392
+ if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
393
+ WebSettingsCompat.setPaymentRequestEnabled(_webView.getSettings(), true);
394
+ Log.d("InAppBrowser", "Payment Request API enabled");
395
+ } else {
396
+ Log.d("InAppBrowser", "Payment Request API not supported on this device");
397
+ }
398
+ }
399
+
400
+ // Set web view background color
401
+ int backgroundColor = _options.getBackgroundColor().equals("white") ? Color.WHITE : Color.BLACK;
402
+ _webView.setBackgroundColor(backgroundColor);
403
+
404
+ // Set text zoom if specified in options
405
+ if (_options.getTextZoom() > 0) {
406
+ _webView.getSettings().setTextZoom(_options.getTextZoom());
407
+ }
408
+
409
+ _webView.setWebViewClient(new WebViewClient());
410
+
411
+ _webView.setWebChromeClient(
412
+ new WebChromeClient() {
413
+ // Enable file open dialog
414
+ @Override
415
+ public boolean onShowFileChooser(
416
+ WebView webView,
417
+ ValueCallback<Uri[]> filePathCallback,
418
+ FileChooserParams fileChooserParams
419
+ ) {
420
+ // Get the accept type safely
421
+ String acceptType;
422
+ if (
423
+ fileChooserParams.getAcceptTypes() != null &&
424
+ fileChooserParams.getAcceptTypes().length > 0 &&
425
+ !TextUtils.isEmpty(fileChooserParams.getAcceptTypes()[0])
426
+ ) {
427
+ acceptType = fileChooserParams.getAcceptTypes()[0];
428
+ } else {
429
+ acceptType = "*/*";
430
+ }
431
+
432
+ // DEBUG: Log details about the file chooser request
433
+ Log.d("InAppBrowser", "onShowFileChooser called");
434
+ Log.d("InAppBrowser", "Accept type: " + acceptType);
435
+ Log.d("InAppBrowser", "Current URL: " + getUrl());
436
+ Log.d("InAppBrowser", "Original URL: " + (webView.getOriginalUrl() != null ? webView.getOriginalUrl() : "null"));
437
+ Log.d(
438
+ "InAppBrowser",
439
+ "Has camera permission: " +
440
+ (activity != null &&
441
+ activity.checkSelfPermission(android.Manifest.permission.CAMERA) ==
442
+ android.content.pm.PackageManager.PERMISSION_GRANTED)
443
+ );
444
+
445
+ // Check if the file chooser is already open
446
+ if (mFilePathCallback != null) {
447
+ mFilePathCallback.onReceiveValue(null);
448
+ mFilePathCallback = null;
449
+ }
450
+
451
+ mFilePathCallback = filePathCallback;
452
+
453
+ // Direct check for capture attribute in URL (fallback method)
454
+ boolean isCaptureInUrl;
455
+ String captureMode;
456
+ String currentUrl = getUrl();
457
+
458
+ // Look for capture in URL parameters - sometimes the attribute shows up in URL
459
+ if (currentUrl != null && currentUrl.contains("capture=")) {
460
+ isCaptureInUrl = true;
461
+ captureMode = currentUrl.contains("capture=user") ? "user" : "environment";
462
+ Log.d("InAppBrowser", "Found capture in URL: " + captureMode);
463
+ } else {
464
+ captureMode = null;
465
+ isCaptureInUrl = false;
466
+ }
467
+
468
+ // For image inputs, try to detect capture attribute using JavaScript
469
+ if (acceptType.equals("image/*")) {
470
+ // Check if HTML content contains capture attribute on file inputs (synchronous check)
471
+ webView.evaluateJavascript(
472
+ "document.querySelector('input[type=\"file\"][capture]') !== null",
473
+ (hasCaptureValue) -> {
474
+ Log.d("InAppBrowser", "Quick capture check: " + hasCaptureValue);
475
+ if (Boolean.parseBoolean(hasCaptureValue.replace("\"", ""))) {
476
+ Log.d("InAppBrowser", "Found capture attribute in quick check");
477
+ }
478
+ }
479
+ );
480
+
481
+ // Fixed JavaScript with proper error handling
482
+ String js = """
483
+ try {
484
+ (function() {
485
+ var captureAttr = null;
486
+ // Check active element first
487
+ if (document.activeElement &&
488
+ document.activeElement.tagName === 'INPUT' &&
489
+ document.activeElement.type === 'file') {
490
+ if (document.activeElement.hasAttribute('capture')) {
491
+ captureAttr = document.activeElement.getAttribute('capture') || 'environment';
492
+ return captureAttr;
493
+ }
494
+ }
495
+ // Try to find any input with capture attribute
496
+ var inputs = document.querySelectorAll('input[type="file"][capture]');
497
+ if (inputs && inputs.length > 0) {
498
+ captureAttr = inputs[0].getAttribute('capture') || 'environment';
499
+ return captureAttr;
500
+ }
501
+ // Try to extract from HTML attributes
502
+ var allInputs = document.getElementsByTagName('input');
503
+ for (var i = 0; i < allInputs.length; i++) {
504
+ var input = allInputs[i];
505
+ if (input.type === 'file') {
506
+ if (input.hasAttribute('capture')) {
507
+ captureAttr = input.getAttribute('capture') || 'environment';
508
+ return captureAttr;
509
+ }
510
+ // Look for the accept attribute containing image/* as this might be a camera input
511
+ var acceptAttr = input.getAttribute('accept');
512
+ if (acceptAttr && acceptAttr.indexOf('image/*') >= 0) {
513
+ console.log('Found input with image/* accept');
514
+ }
515
+ }
516
+ }
517
+ return '';
518
+ })();
519
+ } catch(e) {
520
+ console.error('Capture detection error:', e);
521
+ return '';
522
+ }
523
+ """;
524
+
525
+ webView.evaluateJavascript(js, (value) -> {
526
+ Log.d("InAppBrowser", "Capture attribute JS result: " + value);
527
+
528
+ // If we already found capture in URL, use that directly
529
+ if (isCaptureInUrl) {
530
+ Log.d("InAppBrowser", "Using capture from URL: " + captureMode);
531
+ launchCamera(captureMode.equals("user"));
532
+ return;
533
+ }
534
+
535
+ // Process JavaScript result
536
+ if (value != null && value.length() > 2) {
537
+ // Clean up the value (remove quotes)
538
+ String captureValue = value.replace("\"", "");
539
+ Log.d("InAppBrowser", "Found capture attribute: " + captureValue);
540
+
541
+ if (!captureValue.isEmpty()) {
542
+ activity.runOnUiThread(() -> launchCamera(captureValue.equals("user")));
543
+ return;
544
+ }
545
+ }
546
+
547
+ // Look for hints in the web page source
548
+ Log.d("InAppBrowser", "Looking for camera hints in page content");
549
+ webView.evaluateJavascript("(function() { return document.documentElement.innerHTML; })()", (htmlSource) -> {
550
+ if (htmlSource != null && htmlSource.length() > 10) {
551
+ boolean hasCameraOrSelfieKeyword =
552
+ htmlSource.contains("capture=") || htmlSource.contains("camera") || htmlSource.contains("selfie");
553
+
554
+ Log.d("InAppBrowser", "Page contains camera keywords: " + hasCameraOrSelfieKeyword);
555
+
556
+ if (
557
+ hasCameraOrSelfieKeyword &&
558
+ currentUrl != null &&
559
+ (currentUrl.contains("selfie") || currentUrl.contains("camera") || currentUrl.contains("photo"))
560
+ ) {
561
+ Log.d("InAppBrowser", "URL suggests camera usage, launching camera");
562
+ activity.runOnUiThread(() -> launchCamera(currentUrl.contains("selfie")));
563
+ return;
564
+ }
565
+ }
566
+
567
+ // If all detection methods fail, fall back to regular file picker
568
+ Log.d("InAppBrowser", "No capture attribute detected, using file picker");
569
+ openFileChooser(
570
+ filePathCallback,
571
+ acceptType,
572
+ fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE
573
+ );
574
+ });
575
+ });
576
+ return true;
577
+ }
578
+
579
+ // For non-image types, use regular file picker
580
+ openFileChooser(filePathCallback, acceptType, fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE);
581
+ return true;
582
+ }
583
+
584
+ /**
585
+ * Launch the camera app for capturing images
586
+ * @param useFrontCamera true to use front camera, false for back camera
587
+ */
588
+ private void launchCamera(boolean useFrontCamera) {
589
+ Log.d("InAppBrowser", "Launching camera, front camera: " + useFrontCamera);
590
+
591
+ // First check if we have camera permission
592
+ if (activity != null && permissionHandler != null) {
593
+ // Create a temporary permission request to check camera permission
594
+ android.webkit.PermissionRequest tempRequest = new android.webkit.PermissionRequest() {
595
+ @Override
596
+ public Uri getOrigin() {
597
+ return Uri.parse("file:///android_asset/");
598
+ }
599
+
600
+ @Override
601
+ public String[] getResources() {
602
+ return new String[] { PermissionRequest.RESOURCE_VIDEO_CAPTURE };
603
+ }
604
+
605
+ @Override
606
+ public void grant(String[] resources) {
607
+ // Permission granted, now launch the camera
608
+ launchCameraWithPermission(useFrontCamera);
609
+ }
610
+
611
+ @Override
612
+ public void deny() {
613
+ // Permission denied, fall back to file picker
614
+ Log.e("InAppBrowser", "Camera permission denied, falling back to file picker");
615
+ fallbackToFilePicker();
616
+ }
617
+ };
618
+
619
+ // Request camera permission through the plugin
620
+ permissionHandler.handleCameraPermissionRequest(tempRequest);
621
+ return;
622
+ }
623
+
624
+ // If we can't request permission, try launching directly
625
+ launchCameraWithPermission(useFrontCamera);
626
+ }
627
+
628
+ /**
629
+ * Launch camera after permission is granted
630
+ */
631
+ private void launchCameraWithPermission(boolean useFrontCamera) {
632
+ try {
633
+ Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
634
+ if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null) {
635
+ File photoFile = null;
636
+ try {
637
+ photoFile = createImageFile();
638
+ } catch (IOException ex) {
639
+ Log.e("InAppBrowser", "Error creating image file", ex);
640
+ fallbackToFilePicker();
641
+ return;
642
+ }
643
+
644
+ if (photoFile != null) {
645
+ tempCameraUri = FileProvider.getUriForFile(
646
+ activity,
647
+ activity.getPackageName() + ".fileprovider",
648
+ photoFile
649
+ );
650
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, tempCameraUri);
651
+
652
+ if (useFrontCamera) {
653
+ takePictureIntent.putExtra("android.intent.extras.CAMERA_FACING", 1);
654
+ }
655
+
656
+ try {
657
+ if (activity instanceof androidx.activity.ComponentActivity) {
658
+ androidx.activity.ComponentActivity componentActivity =
659
+ (androidx.activity.ComponentActivity) activity;
660
+ componentActivity
661
+ .getActivityResultRegistry()
662
+ .register(
663
+ "camera_capture",
664
+ new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
665
+ (result) -> {
666
+ if (result.getResultCode() == Activity.RESULT_OK) {
667
+ if (tempCameraUri != null) {
668
+ mFilePathCallback.onReceiveValue(new Uri[] { tempCameraUri });
669
+ }
670
+ } else {
671
+ mFilePathCallback.onReceiveValue(null);
672
+ }
673
+ mFilePathCallback = null;
674
+ tempCameraUri = null;
675
+ }
676
+ )
677
+ .launch(takePictureIntent);
678
+ } else {
679
+ // Fallback for non-ComponentActivity
680
+ activity.startActivityForResult(takePictureIntent, FILE_CHOOSER_REQUEST_CODE);
681
+ }
682
+ } catch (SecurityException e) {
683
+ Log.e("InAppBrowser", "Security exception launching camera: " + e.getMessage(), e);
684
+ fallbackToFilePicker();
685
+ }
686
+ } else {
687
+ Log.e("InAppBrowser", "Failed to create photo URI, falling back to file picker");
688
+ fallbackToFilePicker();
689
+ }
690
+ }
691
+ } catch (Exception e) {
692
+ Log.e("InAppBrowser", "Camera launch failed: " + e.getMessage(), e);
693
+ fallbackToFilePicker();
694
+ }
695
+ }
696
+
697
+ /**
698
+ * Fall back to file picker when camera launch fails
699
+ */
700
+ private void fallbackToFilePicker() {
701
+ if (mFilePathCallback != null) {
702
+ openFileChooser(mFilePathCallback, "image/*", false);
703
+ }
704
+ }
705
+
706
+ // Grant permissions for cam
707
+ @Override
708
+ public void onPermissionRequest(final PermissionRequest request) {
709
+ Log.i("INAPPBROWSER", "onPermissionRequest " + Arrays.toString(request.getResources()));
710
+ final String[] requestedResources = request.getResources();
711
+ for (String r : requestedResources) {
712
+ Log.i("INAPPBROWSER", "requestedResources " + r);
713
+ if (r.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
714
+ Log.i("INAPPBROWSER", "RESOURCE_VIDEO_CAPTURE req");
715
+ // Store the permission request
716
+ currentPermissionRequest = request;
717
+ // Initiate the permission request through the plugin
718
+ if (permissionHandler != null) {
719
+ permissionHandler.handleCameraPermissionRequest(request);
720
+ }
721
+ return; // Return here to avoid denying the request
722
+ } else if (r.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
723
+ Log.i("INAPPBROWSER", "RESOURCE_AUDIO_CAPTURE req");
724
+ // Store the permission request
725
+ currentPermissionRequest = request;
726
+ // Initiate the permission request through the plugin
727
+ if (permissionHandler != null) {
728
+ permissionHandler.handleMicrophonePermissionRequest(request);
729
+ }
730
+ return; // Return here to avoid denying the request
731
+ }
732
+ }
733
+ // If no matching permission is found, deny the request
734
+ request.deny();
735
+ }
736
+
737
+ @Override
738
+ public void onPermissionRequestCanceled(PermissionRequest request) {
739
+ super.onPermissionRequestCanceled(request);
740
+ Toast.makeText(WebViewDialog.this.activity, "Permission Denied", Toast.LENGTH_SHORT).show();
741
+ // Handle the denied permission
742
+ if (currentPermissionRequest != null) {
743
+ currentPermissionRequest.deny();
744
+ currentPermissionRequest = null;
745
+ }
746
+ }
747
+
748
+ // Handle geolocation permission requests
749
+ @Override
750
+ public void onGeolocationPermissionsShowPrompt(String origin, android.webkit.GeolocationPermissions.Callback callback) {
751
+ Log.i("INAPPBROWSER", "onGeolocationPermissionsShowPrompt for origin: " + origin);
752
+ // Grant geolocation permission automatically for openWebView
753
+ // This allows websites to access location when opened with openWebView
754
+ callback.invoke(origin, true, false);
755
+ }
756
+
757
+ // This method will be called at page load, a good place to inject customizations
758
+ @Override
759
+ public void onProgressChanged(WebView view, int newProgress) {
760
+ super.onProgressChanged(view, newProgress);
761
+
762
+ // When the page is almost loaded, inject our date picker customization
763
+ // Only if materialPicker option is enabled
764
+ if (newProgress > 75 && !datePickerInjected && _options.getMaterialPicker()) {
765
+ injectDatePickerFixes();
766
+ }
767
+ }
768
+
769
+ // Support for Google Pay and popup windows (critical for OR_BIBED_15 fix)
770
+ @Override
771
+ public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, android.os.Message resultMsg) {
772
+ Log.d(
773
+ "InAppBrowser",
774
+ "onCreateWindow called - isUserGesture: " +
775
+ isUserGesture +
776
+ ", GooglePaySupport: " +
777
+ _options.getEnableGooglePaySupport() +
778
+ ", preventDeeplink: " +
779
+ _options.getPreventDeeplink()
780
+ );
781
+
782
+ // When preventDeeplink is false, open target="_blank" links externally
783
+ if (!_options.getPreventDeeplink() && isUserGesture) {
784
+ try {
785
+ WebView.HitTestResult result = view.getHitTestResult();
786
+ String data = result.getExtra();
787
+ if (data != null && !data.isEmpty()) {
788
+ Log.d("InAppBrowser", "Opening target=_blank link externally: " + data);
789
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(data));
790
+ browserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
791
+ _webView.getContext().startActivity(browserIntent);
792
+ return false;
793
+ }
794
+ } catch (Exception e) {
795
+ Log.e("InAppBrowser", "Error opening external link: " + e.getMessage());
796
+ }
797
+ }
798
+
799
+ // Only handle popup windows if Google Pay support is enabled
800
+ if (_options.getEnableGooglePaySupport() && isUserGesture) {
801
+ // Create a new WebView for the popup
802
+ WebView popupWebView = new WebView(activity);
803
+ popupWebView.getSettings().setJavaScriptEnabled(true);
804
+ popupWebView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
805
+ popupWebView.getSettings().setSupportMultipleWindows(true);
806
+
807
+ // Set WebViewClient to handle URL loading and closing
808
+ popupWebView.setWebViewClient(
809
+ new WebViewClient() {
810
+ @Override
811
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
812
+ Log.d("InAppBrowser", "Popup WebView loading URL: " + url);
813
+
814
+ // Handle Google Pay result URLs or close conditions
815
+ if (url.contains("google.com/pay") || url.contains("close") || url.contains("cancel")) {
816
+ Log.d("InAppBrowser", "Closing popup for Google Pay result");
817
+ // Notify the parent WebView and close popup
818
+ activity.runOnUiThread(() -> {
819
+ try {
820
+ if (popupWebView.getParent() != null) {
821
+ ((ViewGroup) popupWebView.getParent()).removeView(popupWebView);
822
+ }
823
+ popupWebView.destroy();
824
+ } catch (Exception e) {
825
+ Log.e("InAppBrowser", "Error closing popup: " + e.getMessage());
826
+ }
827
+ });
828
+ return true;
829
+ }
830
+ return false;
831
+ }
832
+ }
833
+ );
834
+
835
+ // Set up the popup WebView transport
836
+ WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
837
+ transport.setWebView(popupWebView);
838
+ resultMsg.sendToTarget();
839
+
840
+ Log.d("InAppBrowser", "Created popup window for Google Pay");
841
+ return true;
842
+ }
843
+
844
+ return false;
845
+ }
846
+ }
847
+ );
848
+
849
+ Map<String, String> requestHeaders = new HashMap<>();
850
+ if (_options.getHeaders() != null) {
851
+ Iterator<String> keys = _options.getHeaders().keys();
852
+ while (keys.hasNext()) {
853
+ String key = keys.next();
854
+ if (TextUtils.equals(key.toLowerCase(), "user-agent")) {
855
+ _webView.getSettings().setUserAgentString(_options.getHeaders().getString(key));
856
+ } else {
857
+ requestHeaders.put(key, _options.getHeaders().getString(key));
858
+ }
859
+ }
860
+ }
861
+
862
+ _webView.loadUrl(this._options.getUrl(), requestHeaders);
863
+ _webView.requestFocus();
864
+ _webView.requestFocusFromTouch();
865
+
866
+ // Inject JavaScript interface early to ensure it's available immediately
867
+ // This complements the injection in onPageFinished and doUpdateVisitedHistory
868
+ _webView.post(() -> {
869
+ if (_webView != null) {
870
+ injectJavaScriptInterface();
871
+
872
+ // Inject Google Pay support enhancements if enabled
873
+ if (_options.getEnableGooglePaySupport()) {
874
+ injectGooglePayPolyfills();
875
+ }
876
+
877
+ Log.d("InAppBrowser", "JavaScript interface injected early after URL load");
878
+ }
879
+ });
880
+
881
+ setupToolbar();
882
+ setWebViewClient();
883
+
884
+ if (!this._options.isPresentAfterPageLoad()) {
885
+ show();
886
+ _options.getPluginCall().resolve();
887
+ }
888
+ }
889
+
890
+ /**
891
+ * Apply window insets to the WebView to properly handle edge-to-edge display
892
+ * and fix status bar overlap issues on Android 15+
893
+ */
894
+ private void applyInsets() {
895
+ if (_webView == null) {
896
+ return;
897
+ }
898
+
899
+ // Check if we need Android 15+ specific fixes
900
+ boolean isAndroid15Plus = Build.VERSION.SDK_INT >= 35;
901
+
902
+ // Get parent view
903
+ ViewGroup parent = (ViewGroup) _webView.getParent();
904
+
905
+ // Find status bar color view and toolbar for Android 15+ specific handling
906
+ View statusBarColorView = findViewById(R.id.status_bar_color_view);
907
+ View toolbarView = findViewById(R.id.tool_bar);
908
+
909
+ // Special handling for Android 15+
910
+ if (isAndroid15Plus) {
911
+ // Get AppBarLayout which contains the toolbar
912
+ if (toolbarView != null && toolbarView.getParent() instanceof com.google.android.material.appbar.AppBarLayout appBarLayout) {
913
+ // Remove elevation to eliminate shadows (only on Android 15+)
914
+ appBarLayout.setElevation(0);
915
+ appBarLayout.setStateListAnimator(null);
916
+ appBarLayout.setOutlineProvider(null);
917
+
918
+ // Determine background color to use
919
+ int backgroundColor = Color.BLACK; // Default fallback
920
+ if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
921
+ try {
922
+ backgroundColor = Color.parseColor(_options.getToolbarColor());
923
+ } catch (IllegalArgumentException e) {
924
+ Log.e("InAppBrowser", "Invalid toolbar color, using black: " + e.getMessage());
925
+ }
926
+ } else {
927
+ // Follow system theme if no color specified
928
+ boolean isDarkTheme = isDarkThemeEnabled();
929
+ backgroundColor = isDarkTheme ? Color.BLACK : Color.WHITE;
930
+ }
931
+
932
+ // Apply fixes for Android 15+ using a delayed post
933
+ final int finalBgColor = backgroundColor;
934
+ _webView.post(() -> {
935
+ // Get status bar height
936
+ int statusBarHeight = 0;
937
+ int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
938
+ if (resourceId > 0) {
939
+ statusBarHeight = getContext().getResources().getDimensionPixelSize(resourceId);
940
+ }
941
+
942
+ // Fix status bar view
943
+ if (statusBarColorView != null) {
944
+ ViewGroup.LayoutParams params = statusBarColorView.getLayoutParams();
945
+ params.height = statusBarHeight;
946
+ statusBarColorView.setLayoutParams(params);
947
+ statusBarColorView.setBackgroundColor(finalBgColor);
948
+ statusBarColorView.setVisibility(View.VISIBLE);
949
+ }
950
+
951
+ // Fix AppBarLayout position
952
+ ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) appBarLayout.getLayoutParams();
953
+ params.topMargin = statusBarHeight;
954
+ appBarLayout.setLayoutParams(params);
955
+ appBarLayout.setBackgroundColor(finalBgColor);
956
+ View contentBrowserLayout = findViewById(R.id.content_browser_layout);
957
+ View parentContainer = findViewById(android.R.id.content);
958
+ if (contentBrowserLayout == null || parentContainer == null) {
959
+ Log.w("InAppBrowser", "Required views not found for height calculation");
960
+ return;
961
+ }
962
+
963
+ ViewGroup.LayoutParams layoutParams = contentBrowserLayout.getLayoutParams();
964
+ if (!(layoutParams instanceof ViewGroup.MarginLayoutParams)) {
965
+ Log.w("InAppBrowser", "Content browser layout does not support margins");
966
+ return;
967
+ }
968
+ ViewGroup.MarginLayoutParams mlpContentBrowserLayout = (ViewGroup.MarginLayoutParams) layoutParams;
969
+
970
+ int parentHeight = parentContainer.getHeight();
971
+ int appBarHeight = appBarLayout.getHeight(); // can be 0 if not visible with the toolbar type BLANK
972
+
973
+ if (parentHeight <= 0) {
974
+ Log.w("InAppBrowser", "Parent dimensions not yet available");
975
+ return;
976
+ }
977
+
978
+ // Recompute the height of the content browser to be able to set margin bottom as we want to
979
+ mlpContentBrowserLayout.height = parentHeight - (statusBarHeight + appBarHeight);
980
+ contentBrowserLayout.setLayoutParams(mlpContentBrowserLayout);
981
+ });
982
+ }
983
+ }
984
+
985
+ // Apply system insets to WebView content view (compatible with all Android versions)
986
+ ViewCompat.setOnApplyWindowInsetsListener(_webView, (v, windowInsets) -> {
987
+ Insets bars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
988
+ Insets ime = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
989
+ Boolean keyboardVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime());
990
+
991
+ ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
992
+
993
+ // Apply safe margin inset to bottom margin if enabled in options or fallback to 0px
994
+ int navBottom = _options.getEnabledSafeMargin() ? bars.bottom : 0;
995
+
996
+ // Apply top inset only if useTopInset option is enabled or fallback to 0px
997
+ int navTop = _options.getUseTopInset() ? bars.top : 0;
998
+
999
+ // Avoid double-applying top inset; AppBar/status bar handled above on Android 15+
1000
+ mlp.topMargin = isAndroid15Plus ? 0 : navTop;
1001
+
1002
+ // Apply larger of navigation bar or keyboard inset to bottom margin
1003
+ mlp.bottomMargin = Math.max(navBottom, ime.bottom);
1004
+
1005
+ mlp.leftMargin = bars.left;
1006
+ mlp.rightMargin = bars.right;
1007
+ v.setLayoutParams(mlp);
1008
+
1009
+ return WindowInsetsCompat.CONSUMED;
1010
+ });
1011
+
1012
+ // Handle window decoration - version-specific handling
1013
+ if (getWindow() != null) {
1014
+ if (isAndroid15Plus) {
1015
+ // Android 15+: Use edge-to-edge with proper insets handling
1016
+ getWindow().setDecorFitsSystemWindows(false);
1017
+ getWindow().setStatusBarColor(Color.TRANSPARENT);
1018
+ getWindow().setNavigationBarColor(Color.TRANSPARENT);
1019
+
1020
+ WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView());
1021
+
1022
+ // Set status bar text color
1023
+ if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
1024
+ try {
1025
+ int backgroundColor = Color.parseColor(_options.getToolbarColor());
1026
+ boolean isDarkBackground = isDarkColor(backgroundColor);
1027
+ controller.setAppearanceLightStatusBars(!isDarkBackground);
1028
+ } catch (IllegalArgumentException e) {
1029
+ // Ignore color parsing errors
1030
+ }
1031
+ }
1032
+ } else if (Build.VERSION.SDK_INT >= 30) {
1033
+ // Android 11-14: Keep navigation bar transparent but respect status bar
1034
+ getWindow().setNavigationBarColor(Color.TRANSPARENT);
1035
+
1036
+ WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView());
1037
+
1038
+ // Set status bar color to match toolbar or use system default
1039
+ if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
1040
+ try {
1041
+ int toolbarColor = Color.parseColor(_options.getToolbarColor());
1042
+ getWindow().setStatusBarColor(toolbarColor);
1043
+ boolean isDarkBackground = isDarkColor(toolbarColor);
1044
+ controller.setAppearanceLightStatusBars(!isDarkBackground);
1045
+ } catch (IllegalArgumentException e) {
1046
+ // Follow system theme if color parsing fails
1047
+ boolean isDarkTheme = isDarkThemeEnabled();
1048
+ int statusBarColor = isDarkTheme ? Color.BLACK : Color.WHITE;
1049
+ getWindow().setStatusBarColor(statusBarColor);
1050
+ controller.setAppearanceLightStatusBars(!isDarkTheme);
1051
+ }
1052
+ } else {
1053
+ // Follow system theme if no toolbar color provided
1054
+ boolean isDarkTheme = isDarkThemeEnabled();
1055
+ int statusBarColor = isDarkTheme ? Color.BLACK : Color.WHITE;
1056
+ getWindow().setStatusBarColor(statusBarColor);
1057
+ controller.setAppearanceLightStatusBars(!isDarkTheme);
1058
+ }
1059
+ } else {
1060
+ // Pre-Android 11: Use deprecated flags for edge-to-edge navigation bar only
1061
+ getWindow()
1062
+ .getDecorView()
1063
+ .setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
1064
+
1065
+ getWindow().setNavigationBarColor(Color.TRANSPARENT);
1066
+
1067
+ // Set status bar color to match toolbar
1068
+ if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
1069
+ try {
1070
+ int toolbarColor = Color.parseColor(_options.getToolbarColor());
1071
+ getWindow().setStatusBarColor(toolbarColor);
1072
+ } catch (IllegalArgumentException e) {
1073
+ // Use system default
1074
+ }
1075
+ }
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ public void postMessageToJS(Object detail) {
1081
+ if (_webView != null) {
1082
+ try {
1083
+ JSONObject jsonObject = new JSONObject();
1084
+ jsonObject.put("detail", detail);
1085
+ String jsonDetail = jsonObject.toString();
1086
+ String script = String.format("window.dispatchEvent(new CustomEvent('messageFromNative', %s));", jsonDetail);
1087
+ _webView.post(() -> {
1088
+ if (_webView != null) {
1089
+ _webView.evaluateJavascript(script, null);
1090
+ }
1091
+ });
1092
+ } catch (Exception e) {
1093
+ Log.e("postMessageToJS", "Error sending message to JS: " + e.getMessage());
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ private void injectJavaScriptInterface() {
1099
+ if (_webView == null) {
1100
+ Log.w("InAppBrowser", "Cannot inject JavaScript interface - WebView is null");
1101
+ return;
1102
+ }
1103
+
1104
+ try {
1105
+ String script = """
1106
+ (function() {
1107
+ if (window.AndroidInterface) {
1108
+ // Create mobileApp object for backward compatibility
1109
+ if (!window.mobileApp) {
1110
+ window.mobileApp = {
1111
+ postMessage: function(message) {
1112
+ try {
1113
+ var msg = typeof message === 'string' ? message : JSON.stringify(message);
1114
+ window.AndroidInterface.postMessage(msg);
1115
+ } catch(e) {
1116
+ console.error('Error in mobileApp.postMessage:', e);
1117
+ }
1118
+ },
1119
+ close: function() {
1120
+ try {
1121
+ window.AndroidInterface.close();
1122
+ } catch(e) {
1123
+ console.error('Error in mobileApp.close:', e);
1124
+ }
1125
+ }
1126
+ };
1127
+ }
1128
+ }
1129
+ // Override window.print function to use our PrintInterface
1130
+ if (window.PrintInterface) {
1131
+ window.print = function() {
1132
+ try {
1133
+ window.PrintInterface.print();
1134
+ } catch(e) {
1135
+ console.error('Error in print:', e);
1136
+ }
1137
+ };
1138
+ }
1139
+ })();
1140
+ """;
1141
+
1142
+ _webView.post(() -> {
1143
+ if (_webView != null) {
1144
+ try {
1145
+ _webView.evaluateJavascript(script, null);
1146
+ } catch (Exception e) {
1147
+ Log.e("InAppBrowser", "Error injecting JavaScript interface: " + e.getMessage());
1148
+ }
1149
+ }
1150
+ });
1151
+ } catch (Exception e) {
1152
+ Log.e("InAppBrowser", "Error preparing JavaScript interface: " + e.getMessage());
1153
+ }
1154
+ }
1155
+
1156
+ /**
1157
+ * Injects JavaScript polyfills and enhancements for Google Pay support
1158
+ * Helps resolve OR_BIBED_15 errors by ensuring proper cross-origin handling
1159
+ */
1160
+ private void injectGooglePayPolyfills() {
1161
+ if (_webView == null) {
1162
+ Log.w("InAppBrowser", "Cannot inject Google Pay polyfills - WebView is null");
1163
+ return;
1164
+ }
1165
+
1166
+ try {
1167
+ String googlePayScript = """
1168
+ (function() {
1169
+ console.log('[InAppBrowser] Injecting Google Pay support enhancements');
1170
+
1171
+ // Enhance window.open to work better with Google Pay popups
1172
+ const originalWindowOpen = window.open;
1173
+ window.open = function(url, target, features) {
1174
+ console.log('[InAppBrowser] Enhanced window.open called:', url, target, features);
1175
+
1176
+ // For Google Pay URLs, ensure they open in a new context
1177
+ if (url && (url.includes('google.com/pay') || url.includes('accounts.google.com'))) {
1178
+ console.log('[InAppBrowser] Google Pay popup detected, using enhanced handling');
1179
+ // Let the native WebView handle this via onCreateWindow
1180
+ return originalWindowOpen.call(window, url, '_blank', features);
1181
+ }
1182
+
1183
+ return originalWindowOpen.call(window, url, target, features);
1184
+ };
1185
+
1186
+ // Ensure proper Payment Request API context
1187
+ if (window.PaymentRequest) {
1188
+ console.log('[InAppBrowser] Payment Request API available');
1189
+
1190
+ // Wrap PaymentRequest constructor to add better error handling
1191
+ const OriginalPaymentRequest = window.PaymentRequest;
1192
+ window.PaymentRequest = function(methodData, details, options) {
1193
+ console.log('[InAppBrowser] PaymentRequest created with enhanced error handling');
1194
+ const request = new OriginalPaymentRequest(methodData, details, options);
1195
+
1196
+ // Override show method to handle popup blocking issues
1197
+ const originalShow = request.show;
1198
+ request.show = function() {
1199
+ console.log('[InAppBrowser] PaymentRequest.show() called');
1200
+ return originalShow.call(this).catch((error) => {
1201
+ console.error('[InAppBrowser] PaymentRequest error:', error);
1202
+ if (error.name === 'SecurityError' || error.message.includes('popup')) {
1203
+ console.log('[InAppBrowser] Attempting to handle popup blocking issue');
1204
+ }
1205
+ throw error;
1206
+ });
1207
+ };
1208
+
1209
+ return request;
1210
+ };
1211
+
1212
+ // Copy static methods
1213
+ Object.setPrototypeOf(window.PaymentRequest, OriginalPaymentRequest);
1214
+ Object.defineProperty(window.PaymentRequest, 'prototype', {
1215
+ value: OriginalPaymentRequest.prototype
1216
+ });
1217
+ }
1218
+
1219
+ // Add meta tag to ensure proper cross-origin handling if not present
1220
+ if (!document.querySelector('meta[http-equiv="Cross-Origin-Opener-Policy"]')) {
1221
+ const meta = document.createElement('meta');
1222
+ meta.setAttribute('http-equiv', 'Cross-Origin-Opener-Policy');
1223
+ meta.setAttribute('content', 'same-origin-allow-popups');
1224
+ if (document.head) {
1225
+ document.head.appendChild(meta);
1226
+ console.log('[InAppBrowser] Added Cross-Origin-Opener-Policy meta tag');
1227
+ }
1228
+ }
1229
+
1230
+ console.log('[InAppBrowser] Google Pay support enhancements complete');
1231
+ })();
1232
+ """;
1233
+
1234
+ _webView.post(() -> {
1235
+ if (_webView != null) {
1236
+ try {
1237
+ _webView.evaluateJavascript(googlePayScript, (result) -> {
1238
+ Log.d("InAppBrowser", "Google Pay polyfills injected successfully");
1239
+ });
1240
+ } catch (Exception e) {
1241
+ Log.e("InAppBrowser", "Error injecting Google Pay polyfills: " + e.getMessage());
1242
+ }
1243
+ }
1244
+ });
1245
+ } catch (Exception e) {
1246
+ Log.e("InAppBrowser", "Error preparing Google Pay polyfills: " + e.getMessage());
1247
+ }
1248
+ }
1249
+
1250
+ private void injectPreShowScript() {
1251
+ // String script =
1252
+ // "import('https://unpkg.com/darkreader@4.9.89/darkreader.js').then(() => {DarkReader.enable({ brightness: 100, contrast: 90, sepia: 10 });window.PreLoadScriptInterface.finished()})";
1253
+
1254
+ if (preShowSemaphore != null) {
1255
+ return;
1256
+ }
1257
+
1258
+ String script = String.format(
1259
+ """
1260
+ async function preShowFunction() {
1261
+ %s
1262
+ }
1263
+ preShowFunction()
1264
+ .then(() => window.PreShowScriptInterface.success())
1265
+ .catch(err => {
1266
+ console.error('Pre show error', err);
1267
+ window.PreShowScriptInterface.error(JSON.stringify(err, Object.getOwnPropertyNames(err)));
1268
+ });
1269
+ """,
1270
+ _options.getPreShowScript()
1271
+ );
1272
+
1273
+ Log.i("InjectPreShowScript", String.format("PreShowScript script:\n%s", script));
1274
+
1275
+ preShowSemaphore = new Semaphore(0);
1276
+ activity.runOnUiThread(
1277
+ new Runnable() {
1278
+ @Override
1279
+ public void run() {
1280
+ if (_webView != null) {
1281
+ _webView.evaluateJavascript(script, null);
1282
+ } else {
1283
+ // If WebView is null, release semaphore to prevent deadlock
1284
+ if (preShowSemaphore != null) {
1285
+ preShowSemaphore.release();
1286
+ }
1287
+ }
1288
+ }
1289
+ }
1290
+ );
1291
+
1292
+ try {
1293
+ if (!preShowSemaphore.tryAcquire(10, TimeUnit.SECONDS)) {
1294
+ Log.e("InjectPreShowScript", "PreShowScript running for over 10 seconds. The plugin will not wait any longer!");
1295
+ return;
1296
+ }
1297
+ if (preShowError != null && !preShowError.isEmpty()) {
1298
+ Log.e("InjectPreShowScript", "Error within the user-provided preShowFunction: " + preShowError);
1299
+ }
1300
+ } catch (InterruptedException e) {
1301
+ Log.e("InjectPreShowScript", "Error when calling InjectPreShowScript: " + e.getMessage());
1302
+ } finally {
1303
+ preShowSemaphore = null;
1304
+ preShowError = null;
1305
+ }
1306
+ }
1307
+
1308
+ private void openFileChooser(ValueCallback<Uri[]> filePathCallback, String acceptType, boolean isMultiple) {
1309
+ mFilePathCallback = filePathCallback;
1310
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
1311
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
1312
+
1313
+ // Fix MIME type handling
1314
+ if (acceptType == null || acceptType.isEmpty() || acceptType.equals("undefined")) {
1315
+ acceptType = "*/*";
72
1316
  } else {
73
- requestHeaders.put(key, _options.getHeaders().getString(key));
1317
+ // Handle common web input accept types
1318
+ if (acceptType.equals("image/*")) {
1319
+ // Keep as is - image/*
1320
+ } else if (acceptType.contains("image/")) {
1321
+ // Specific image type requested but keep it general for better compatibility
1322
+ acceptType = "image/*";
1323
+ } else if (acceptType.equals("audio/*") || acceptType.contains("audio/")) {
1324
+ acceptType = "audio/*";
1325
+ } else if (acceptType.equals("video/*") || acceptType.contains("video/")) {
1326
+ acceptType = "video/*";
1327
+ } else if (acceptType.startsWith(".") || acceptType.contains(",")) {
1328
+ // Handle file extensions like ".pdf, .docx" by using a general mime type
1329
+ if (acceptType.contains(".pdf")) {
1330
+ acceptType = "application/pdf";
1331
+ } else if (acceptType.contains(".doc") || acceptType.contains(".docx")) {
1332
+ acceptType = "application/msword";
1333
+ } else if (acceptType.contains(".xls") || acceptType.contains(".xlsx")) {
1334
+ acceptType = "application/vnd.ms-excel";
1335
+ } else if (acceptType.contains(".txt") || acceptType.contains(".text")) {
1336
+ acceptType = "text/plain";
1337
+ } else {
1338
+ // Default for extension lists
1339
+ acceptType = "*/*";
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ Log.d("InAppBrowser", "File picker using MIME type: " + acceptType);
1345
+ intent.setType(acceptType);
1346
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultiple);
1347
+
1348
+ try {
1349
+ if (activity instanceof androidx.activity.ComponentActivity) {
1350
+ androidx.activity.ComponentActivity componentActivity = (androidx.activity.ComponentActivity) activity;
1351
+ componentActivity
1352
+ .getActivityResultRegistry()
1353
+ .register(
1354
+ "file_chooser",
1355
+ new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
1356
+ (result) -> {
1357
+ if (result.getResultCode() == Activity.RESULT_OK) {
1358
+ Intent data = result.getData();
1359
+ if (data != null) {
1360
+ if (data.getClipData() != null) {
1361
+ // Handle multiple files
1362
+ int count = data.getClipData().getItemCount();
1363
+ Uri[] results = new Uri[count];
1364
+ for (int i = 0; i < count; i++) {
1365
+ results[i] = data.getClipData().getItemAt(i).getUri();
1366
+ }
1367
+ mFilePathCallback.onReceiveValue(results);
1368
+ } else if (data.getData() != null) {
1369
+ // Handle single file
1370
+ mFilePathCallback.onReceiveValue(new Uri[] { data.getData() });
1371
+ }
1372
+ }
1373
+ } else {
1374
+ mFilePathCallback.onReceiveValue(null);
1375
+ }
1376
+ mFilePathCallback = null;
1377
+ }
1378
+ )
1379
+ .launch(Intent.createChooser(intent, "Select File"));
1380
+ } else {
1381
+ // Fallback for non-ComponentActivity
1382
+ activity.startActivityForResult(Intent.createChooser(intent, "Select File"), FILE_CHOOSER_REQUEST_CODE);
1383
+ }
1384
+ } catch (ActivityNotFoundException e) {
1385
+ // If no app can handle the specific MIME type, try with a more generic one
1386
+ Log.e("InAppBrowser", "No app available for type: " + acceptType + ", trying with */*");
1387
+ intent.setType("*/*");
1388
+ try {
1389
+ if (activity instanceof androidx.activity.ComponentActivity) {
1390
+ androidx.activity.ComponentActivity componentActivity = (androidx.activity.ComponentActivity) activity;
1391
+ componentActivity
1392
+ .getActivityResultRegistry()
1393
+ .register(
1394
+ "file_chooser",
1395
+ new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
1396
+ (result) -> {
1397
+ if (result.getResultCode() == Activity.RESULT_OK) {
1398
+ Intent data = result.getData();
1399
+ if (data != null) {
1400
+ if (data.getClipData() != null) {
1401
+ // Handle multiple files
1402
+ int count = data.getClipData().getItemCount();
1403
+ Uri[] results = new Uri[count];
1404
+ for (int i = 0; i < count; i++) {
1405
+ results[i] = data.getClipData().getItemAt(i).getUri();
1406
+ }
1407
+ mFilePathCallback.onReceiveValue(results);
1408
+ } else if (data.getData() != null) {
1409
+ // Handle single file
1410
+ mFilePathCallback.onReceiveValue(new Uri[] { data.getData() });
1411
+ }
1412
+ }
1413
+ } else {
1414
+ mFilePathCallback.onReceiveValue(null);
1415
+ }
1416
+ mFilePathCallback = null;
1417
+ }
1418
+ )
1419
+ .launch(Intent.createChooser(intent, "Select File"));
1420
+ } else {
1421
+ // Fallback for non-ComponentActivity
1422
+ activity.startActivityForResult(Intent.createChooser(intent, "Select File"), FILE_CHOOSER_REQUEST_CODE);
1423
+ }
1424
+ } catch (ActivityNotFoundException ex) {
1425
+ // If still failing, report error
1426
+ Log.e("InAppBrowser", "No app can handle file picker", ex);
1427
+ if (mFilePathCallback != null) {
1428
+ mFilePathCallback.onReceiveValue(null);
1429
+ mFilePathCallback = null;
1430
+ }
1431
+ }
74
1432
  }
75
- }
76
1433
  }
77
1434
 
78
- _webView.loadUrl(this._options.getUrl(), requestHeaders);
79
- _webView.requestFocus();
80
- _webView.requestFocusFromTouch();
1435
+ public void reload() {
1436
+ if (_webView == null) {
1437
+ Log.w("InAppBrowser", "Cannot reload - WebView is null");
1438
+ return;
1439
+ }
81
1440
 
82
- setupToolbar();
83
- setWebViewClient();
1441
+ try {
1442
+ // First stop any ongoing loading
1443
+ _webView.stopLoading();
84
1444
 
85
- if (!this._options.isPresentAfterPageLoad()) {
86
- show();
87
- _options.getPluginCall().resolve();
1445
+ // Check if there's a URL to reload
1446
+ String currentUrl = getUrl();
1447
+ if (currentUrl != null && !currentUrl.equals("about:blank")) {
1448
+ // Reload the current page
1449
+ _webView.reload();
1450
+ Log.d("InAppBrowser", "Reloading page: " + currentUrl);
1451
+ } else if (_options != null && _options.getUrl() != null) {
1452
+ // If webView URL is null but we have an initial URL, load that
1453
+ setUrl(_options.getUrl());
1454
+ Log.d("InAppBrowser", "Loading initial URL: " + _options.getUrl());
1455
+ } else {
1456
+ Log.w("InAppBrowser", "Cannot reload - no valid URL available");
1457
+ }
1458
+ } catch (Exception e) {
1459
+ Log.e("InAppBrowser", "Error during reload: " + e.getMessage());
1460
+ }
88
1461
  }
89
- }
90
1462
 
91
- public void setUrl(String url) {
92
- Map<String, String> requestHeaders = new HashMap<>();
93
- if (_options.getHeaders() != null) {
94
- Iterator<String> keys = _options.getHeaders().keys();
95
- while (keys.hasNext()) {
96
- String key = keys.next();
97
- if (TextUtils.equals(key, "User-Agent")) {
98
- _webView
99
- .getSettings()
100
- .setUserAgentString(_options.getHeaders().getString(key));
1463
+ public void destroy() {
1464
+ if (_webView != null) {
1465
+ _webView.destroy();
1466
+ }
1467
+ }
1468
+
1469
+ public String getUrl() {
1470
+ try {
1471
+ WebView webView = _webView;
1472
+ if (webView != null) {
1473
+ String url = webView.getUrl();
1474
+ return url != null ? url : "";
1475
+ }
1476
+ } catch (Exception e) {
1477
+ Log.w("InAppBrowser", "Error getting URL: " + e.getMessage());
1478
+ }
1479
+ return "";
1480
+ }
1481
+
1482
+ public void executeScript(String script) {
1483
+ if (_webView == null) {
1484
+ Log.w("InAppBrowser", "Cannot execute script - WebView is null");
1485
+ return;
1486
+ }
1487
+
1488
+ if (script == null || script.trim().isEmpty()) {
1489
+ Log.w("InAppBrowser", "Cannot execute empty script");
1490
+ return;
1491
+ }
1492
+
1493
+ try {
1494
+ _webView.evaluateJavascript(script, null);
1495
+ } catch (Exception e) {
1496
+ Log.e("InAppBrowser", "Error executing script: " + e.getMessage());
1497
+ }
1498
+ }
1499
+
1500
+ public void setUrl(String url) {
1501
+ if (_webView == null) {
1502
+ Log.w("InAppBrowser", "Cannot set URL - WebView is null");
1503
+ return;
1504
+ }
1505
+
1506
+ if (url == null || url.trim().isEmpty()) {
1507
+ Log.w("InAppBrowser", "Cannot set empty URL");
1508
+ return;
1509
+ }
1510
+
1511
+ try {
1512
+ Map<String, String> requestHeaders = new HashMap<>();
1513
+ if (_options.getHeaders() != null) {
1514
+ Iterator<String> keys = _options.getHeaders().keys();
1515
+ while (keys.hasNext()) {
1516
+ String key = keys.next();
1517
+ if (TextUtils.equals(key.toLowerCase(), "user-agent")) {
1518
+ _webView.getSettings().setUserAgentString(_options.getHeaders().getString(key));
1519
+ } else {
1520
+ requestHeaders.put(key, _options.getHeaders().getString(key));
1521
+ }
1522
+ }
1523
+ }
1524
+ _webView.loadUrl(url, requestHeaders);
1525
+ } catch (Exception e) {
1526
+ Log.e("InAppBrowser", "Error setting URL: " + e.getMessage());
1527
+ }
1528
+ }
1529
+
1530
+ private void setTitle(String newTitleText) {
1531
+ TextView textView = (TextView) _toolbar.findViewById(R.id.titleText);
1532
+ if (_options.getVisibleTitle()) {
1533
+ textView.setText(newTitleText);
1534
+ } else {
1535
+ textView.setText("");
1536
+ }
1537
+ }
1538
+
1539
+ private void setupToolbar() {
1540
+ _toolbar = findViewById(R.id.tool_bar);
1541
+
1542
+ // Apply toolbar color early, for ALL toolbar types, before any view configuration
1543
+ if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
1544
+ try {
1545
+ int toolbarColor = Color.parseColor(_options.getToolbarColor());
1546
+ _toolbar.setBackgroundColor(toolbarColor);
1547
+
1548
+ // Get toolbar title and ensure it gets the right color
1549
+ TextView titleText = _toolbar.findViewById(R.id.titleText);
1550
+
1551
+ // Determine icon and text color
1552
+ int iconColor;
1553
+ if (_options.getToolbarTextColor() != null && !_options.getToolbarTextColor().isEmpty()) {
1554
+ try {
1555
+ iconColor = Color.parseColor(_options.getToolbarTextColor());
1556
+ } catch (IllegalArgumentException e) {
1557
+ // Fallback to automatic detection if parsing fails
1558
+ boolean isDarkBackground = isDarkColor(toolbarColor);
1559
+ iconColor = isDarkBackground ? Color.WHITE : Color.BLACK;
1560
+ }
1561
+ } else {
1562
+ // No explicit toolbarTextColor, use automatic detection based on background
1563
+ boolean isDarkBackground = isDarkColor(toolbarColor);
1564
+ iconColor = isDarkBackground ? Color.WHITE : Color.BLACK;
1565
+ }
1566
+
1567
+ // Store for later use with navigation buttons
1568
+ this.iconColor = iconColor;
1569
+
1570
+ // Set title text color directly
1571
+ titleText.setTextColor(iconColor);
1572
+
1573
+ // Apply colors to all buttons
1574
+ applyColorToAllButtons(toolbarColor, iconColor);
1575
+
1576
+ // Also ensure status bar gets the color
1577
+ if (getWindow() != null) {
1578
+ // Set status bar color
1579
+ getWindow().setStatusBarColor(toolbarColor);
1580
+
1581
+ // Determine proper status bar text color (light or dark icons)
1582
+ boolean isDarkBackground = isDarkColor(toolbarColor);
1583
+ WindowInsetsControllerCompat insetsController = new WindowInsetsControllerCompat(
1584
+ getWindow(),
1585
+ getWindow().getDecorView()
1586
+ );
1587
+ insetsController.setAppearanceLightStatusBars(!isDarkBackground);
1588
+ }
1589
+ } catch (IllegalArgumentException e) {
1590
+ Log.e("InAppBrowser", "Invalid toolbar color: " + _options.getToolbarColor());
1591
+ }
1592
+ }
1593
+
1594
+ ImageButton closeButtonView = _toolbar.findViewById(R.id.closeButton);
1595
+ closeButtonView.setOnClickListener(
1596
+ new View.OnClickListener() {
1597
+ @Override
1598
+ public void onClick(View view) {
1599
+ // if closeModal true then display a native modal to check if the user is sure to close the browser
1600
+ if (_options.getCloseModal()) {
1601
+ new AlertDialog.Builder(_context)
1602
+ .setTitle(_options.getCloseModalTitle())
1603
+ .setMessage(_options.getCloseModalDescription())
1604
+ .setPositiveButton(
1605
+ _options.getCloseModalOk(),
1606
+ new OnClickListener() {
1607
+ public void onClick(DialogInterface dialog, int which) {
1608
+ // Close button clicked, do something
1609
+ String currentUrl = getUrl();
1610
+ dismiss();
1611
+ if (_options != null && _options.getCallbacks() != null) {
1612
+ // Notify that confirm was clicked
1613
+ _options.getCallbacks().confirmBtnClicked(currentUrl);
1614
+ _options.getCallbacks().closeEvent(currentUrl);
1615
+ }
1616
+ }
1617
+ }
1618
+ )
1619
+ .setNegativeButton(_options.getCloseModalCancel(), null)
1620
+ .show();
1621
+ } else {
1622
+ String currentUrl = getUrl();
1623
+ dismiss();
1624
+ if (_options != null && _options.getCallbacks() != null) {
1625
+ _options.getCallbacks().closeEvent(currentUrl);
1626
+ }
1627
+ }
1628
+ }
1629
+ }
1630
+ );
1631
+
1632
+ if (_options.showArrow()) {
1633
+ closeButtonView.setImageResource(R.drawable.arrow_back_enabled);
1634
+ }
1635
+
1636
+ // Handle reload button visibility
1637
+ if (_options.getShowReloadButton() && !TextUtils.equals(_options.getToolbarType(), "activity")) {
1638
+ View reloadButtonView = _toolbar.findViewById(R.id.reloadButton);
1639
+ reloadButtonView.setVisibility(View.VISIBLE);
1640
+ reloadButtonView.setOnClickListener(
1641
+ new View.OnClickListener() {
1642
+ @Override
1643
+ public void onClick(View view) {
1644
+ if (_webView != null) {
1645
+ // First stop any ongoing loading
1646
+ _webView.stopLoading();
1647
+
1648
+ // Check if there's a URL to reload
1649
+ String currentUrl = getUrl();
1650
+ if (currentUrl != null) {
1651
+ // Reload the current page
1652
+ _webView.reload();
1653
+ Log.d("InAppBrowser", "Reloading page: " + currentUrl);
1654
+ } else if (_options.getUrl() != null) {
1655
+ // If webView URL is null but we have an initial URL, load that
1656
+ setUrl(_options.getUrl());
1657
+ Log.d("InAppBrowser", "Loading initial URL: " + _options.getUrl());
1658
+ }
1659
+ }
1660
+ }
1661
+ }
1662
+ );
1663
+ } else {
1664
+ View reloadButtonView = _toolbar.findViewById(R.id.reloadButton);
1665
+ reloadButtonView.setVisibility(View.GONE);
1666
+ }
1667
+
1668
+ if (TextUtils.equals(_options.getToolbarType(), "activity")) {
1669
+ // Activity mode should ONLY have:
1670
+ // 1. Close button
1671
+ // 2. Share button (if shareSubject is provided)
1672
+
1673
+ // Hide all navigation buttons
1674
+ _toolbar.findViewById(R.id.forwardButton).setVisibility(View.GONE);
1675
+ _toolbar.findViewById(R.id.backButton).setVisibility(View.GONE);
1676
+
1677
+ // Hide buttonNearDone
1678
+ ImageButton buttonNearDoneView = _toolbar.findViewById(R.id.buttonNearDone);
1679
+ buttonNearDoneView.setVisibility(View.GONE);
1680
+
1681
+ // In activity mode, always make the share button visible by setting a default shareSubject if not provided
1682
+ if (_options.getShareSubject() == null || _options.getShareSubject().isEmpty()) {
1683
+ _options.setShareSubject("Share");
1684
+ Log.d("InAppBrowser", "Activity mode: Setting default shareSubject");
1685
+ }
1686
+ // Status bar color is already set at the top of this method, no need to set again
1687
+
1688
+ // Share button visibility is handled separately later
1689
+ } else if (TextUtils.equals(_options.getToolbarType(), "navigation")) {
1690
+ ImageButton buttonNearDoneView = _toolbar.findViewById(R.id.buttonNearDone);
1691
+ buttonNearDoneView.setVisibility(View.GONE);
1692
+ // Status bar color is already set at the top of this method, no need to set again
1693
+ } else if (TextUtils.equals(_options.getToolbarType(), "blank")) {
1694
+ _toolbar.setVisibility(View.GONE);
1695
+
1696
+ // Also set window background color to match status bar for blank toolbar
1697
+ View statusBarColorView = findViewById(R.id.status_bar_color_view);
1698
+ if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
1699
+ try {
1700
+ int toolbarColor = Color.parseColor(_options.getToolbarColor());
1701
+ if (getWindow() != null) {
1702
+ getWindow().getDecorView().setBackgroundColor(toolbarColor);
1703
+ }
1704
+ // Also set status bar color view background if available
1705
+ if (statusBarColorView != null) {
1706
+ statusBarColorView.setBackgroundColor(toolbarColor);
1707
+ }
1708
+ } catch (IllegalArgumentException e) {
1709
+ // Fallback to system default if color parsing fails
1710
+ boolean isDarkTheme = isDarkThemeEnabled();
1711
+ int windowBackgroundColor = isDarkTheme ? Color.BLACK : Color.WHITE;
1712
+ if (getWindow() != null) {
1713
+ getWindow().getDecorView().setBackgroundColor(windowBackgroundColor);
1714
+ }
1715
+ // Also set status bar color view background if available
1716
+ if (statusBarColorView != null) {
1717
+ statusBarColorView.setBackgroundColor(windowBackgroundColor);
1718
+ }
1719
+ }
1720
+ } else {
1721
+ // Follow system dark mode
1722
+ boolean isDarkTheme = isDarkThemeEnabled();
1723
+ int windowBackgroundColor = isDarkTheme ? Color.BLACK : Color.WHITE;
1724
+ if (getWindow() != null) {
1725
+ getWindow().getDecorView().setBackgroundColor(windowBackgroundColor);
1726
+ }
1727
+ // Also set status bar color view background if available
1728
+ if (statusBarColorView != null) {
1729
+ statusBarColorView.setBackgroundColor(windowBackgroundColor);
1730
+ }
1731
+ }
101
1732
  } else {
102
- requestHeaders.put(key, _options.getHeaders().getString(key));
103
- }
104
- }
105
- }
106
- _webView.loadUrl(url, requestHeaders);
107
- }
108
-
109
- private void setTitle(String newTitleText) {
110
- TextView textView = (TextView) _toolbar.findViewById(R.id.titleText);
111
- textView.setText(newTitleText);
112
- }
113
-
114
- private void setupToolbar() {
115
- _toolbar = this.findViewById(R.id.tool_bar);
116
- if (!TextUtils.isEmpty(_options.getTitle())) {
117
- this.setTitle(_options.getTitle());
118
- } else {
119
- try {
120
- URI uri = new URI(_options.getUrl());
121
- this.setTitle(uri.getHost());
122
- } catch (URISyntaxException e) {
123
- this.setTitle(_options.getTitle());
124
- }
125
- }
126
-
127
- View backButton = _toolbar.findViewById(R.id.backButton);
128
- backButton.setOnClickListener(
129
- new View.OnClickListener() {
130
- @Override
131
- public void onClick(View view) {
132
- if (_webView.canGoBack()) {
1733
+ _toolbar.findViewById(R.id.forwardButton).setVisibility(View.GONE);
1734
+ _toolbar.findViewById(R.id.backButton).setVisibility(View.GONE);
1735
+
1736
+ // Status bar color is already set at the top of this method, no need to set again
1737
+
1738
+ Options.ButtonNearDone buttonNearDone = _options.getButtonNearDone();
1739
+ if (buttonNearDone != null) {
1740
+ ImageButton buttonNearDoneView = _toolbar.findViewById(R.id.buttonNearDone);
1741
+ buttonNearDoneView.setVisibility(View.VISIBLE);
1742
+
1743
+ // Handle different icon types
1744
+ String iconType = buttonNearDone.getIconType();
1745
+ if ("vector".equals(iconType)) {
1746
+ // Use native Android vector drawable
1747
+ try {
1748
+ String iconName = buttonNearDone.getIcon();
1749
+ // Convert name to Android resource ID (remove file extension if present)
1750
+ if (iconName.endsWith(".xml")) {
1751
+ iconName = iconName.substring(0, iconName.length() - 4);
1752
+ }
1753
+
1754
+ // Get resource ID
1755
+ int resourceId = _context.getResources().getIdentifier(iconName, "drawable", _context.getPackageName());
1756
+
1757
+ if (resourceId != 0) {
1758
+ // Set the vector drawable
1759
+ buttonNearDoneView.setImageResource(resourceId);
1760
+ // Apply color filter
1761
+ buttonNearDoneView.setColorFilter(iconColor);
1762
+ Log.d("InAppBrowser", "Successfully loaded vector drawable: " + iconName);
1763
+ } else {
1764
+ Log.e("InAppBrowser", "Vector drawable not found: " + iconName + ", using fallback");
1765
+ // Fallback to a common system icon
1766
+ buttonNearDoneView.setImageResource(android.R.drawable.ic_menu_info_details);
1767
+ buttonNearDoneView.setColorFilter(iconColor);
1768
+ }
1769
+ } catch (Exception e) {
1770
+ Log.e("InAppBrowser", "Error loading vector drawable: " + e.getMessage());
1771
+ // Fallback to a common system icon
1772
+ buttonNearDoneView.setImageResource(android.R.drawable.ic_menu_info_details);
1773
+ buttonNearDoneView.setColorFilter(iconColor);
1774
+ }
1775
+ } else if ("asset".equals(iconType)) {
1776
+ // Handle SVG from assets
1777
+ AssetManager assetManager = _context.getAssets();
1778
+ InputStream inputStream = null;
1779
+ try {
1780
+ // Try to load from public folder first
1781
+ String iconPath = "public/" + buttonNearDone.getIcon();
1782
+ try {
1783
+ inputStream = assetManager.open(iconPath);
1784
+ } catch (IOException e) {
1785
+ // If not found in public, try root assets
1786
+ try {
1787
+ inputStream = assetManager.open(buttonNearDone.getIcon());
1788
+ } catch (IOException e2) {
1789
+ Log.e("InAppBrowser", "SVG file not found in assets: " + buttonNearDone.getIcon());
1790
+ buttonNearDoneView.setVisibility(View.GONE);
1791
+ return;
1792
+ }
1793
+ }
1794
+
1795
+ // Parse and render SVG
1796
+ SVG svg = SVG.getFromInputStream(inputStream);
1797
+ if (svg == null) {
1798
+ Log.e("InAppBrowser", "Failed to parse SVG icon: " + buttonNearDone.getIcon());
1799
+ buttonNearDoneView.setVisibility(View.GONE);
1800
+ return;
1801
+ }
1802
+
1803
+ // Get the dimensions from options or use SVG's size
1804
+ float width = buttonNearDone.getWidth() > 0 ? buttonNearDone.getWidth() : 24;
1805
+ float height = buttonNearDone.getHeight() > 0 ? buttonNearDone.getHeight() : 24;
1806
+
1807
+ // Get density for proper scaling
1808
+ float density = _context.getResources().getDisplayMetrics().density;
1809
+ int targetWidth = Math.round(width * density);
1810
+ int targetHeight = Math.round(height * density);
1811
+
1812
+ // Set document size
1813
+ svg.setDocumentWidth(targetWidth);
1814
+ svg.setDocumentHeight(targetHeight);
1815
+
1816
+ // Create a bitmap and render SVG to it for better quality
1817
+ Bitmap bitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
1818
+ Canvas canvas = new Canvas(bitmap);
1819
+ svg.renderToCanvas(canvas);
1820
+
1821
+ // Apply color filter to the bitmap
1822
+ Paint paint = new Paint();
1823
+ paint.setColorFilter(new PorterDuffColorFilter(iconColor, PorterDuff.Mode.SRC_IN));
1824
+ Canvas colorFilterCanvas = new Canvas(bitmap);
1825
+ colorFilterCanvas.drawBitmap(bitmap, 0, 0, paint);
1826
+
1827
+ // Set the colored bitmap as image
1828
+ buttonNearDoneView.setImageBitmap(bitmap);
1829
+ buttonNearDoneView.setScaleType(ImageView.ScaleType.FIT_CENTER);
1830
+ buttonNearDoneView.setPadding(12, 12, 12, 12); // Standard button padding
1831
+ } catch (SVGParseException e) {
1832
+ Log.e("InAppBrowser", "Error loading SVG icon: " + e.getMessage(), e);
1833
+ buttonNearDoneView.setVisibility(View.GONE);
1834
+ } finally {
1835
+ if (inputStream != null) {
1836
+ try {
1837
+ inputStream.close();
1838
+ } catch (IOException e) {
1839
+ Log.e("InAppBrowser", "Error closing input stream: " + e.getMessage());
1840
+ }
1841
+ }
1842
+ }
1843
+ } else {
1844
+ // Default fallback or unsupported type
1845
+ Log.e("InAppBrowser", "Unsupported icon type: " + iconType);
1846
+ buttonNearDoneView.setVisibility(View.GONE);
1847
+ }
1848
+
1849
+ // Set the click listener
1850
+ buttonNearDoneView.setOnClickListener((view) -> _options.getCallbacks().buttonNearDoneClicked());
1851
+ } else {
1852
+ ImageButton buttonNearDoneView = _toolbar.findViewById(R.id.buttonNearDone);
1853
+ buttonNearDoneView.setVisibility(View.GONE);
1854
+ }
1855
+ }
1856
+
1857
+ // Add share button functionality
1858
+ ImageButton shareButton = _toolbar.findViewById(R.id.shareButton);
1859
+ if (_options.getShareSubject() != null && !_options.getShareSubject().isEmpty()) {
1860
+ shareButton.setVisibility(View.VISIBLE);
1861
+ Log.d("InAppBrowser", "Share button should be visible, shareSubject: " + _options.getShareSubject());
1862
+
1863
+ // Apply the same color filter as other buttons to ensure visibility
1864
+ shareButton.setColorFilter(iconColor);
1865
+
1866
+ // The color filter is now applied in applyColorToAllButtons
1867
+ shareButton.setOnClickListener((view) -> {
1868
+ JSObject shareDisclaimer = _options.getShareDisclaimer();
1869
+ if (shareDisclaimer != null) {
1870
+ new AlertDialog.Builder(_context)
1871
+ .setTitle(shareDisclaimer.getString("title", "Title"))
1872
+ .setMessage(shareDisclaimer.getString("message", "Message"))
1873
+ .setPositiveButton(shareDisclaimer.getString("confirmBtn", "Confirm"), (dialog, which) -> {
1874
+ // Notify that confirm was clicked
1875
+ String currentUrl = getUrl();
1876
+ _options.getCallbacks().confirmBtnClicked(currentUrl);
1877
+ shareUrl();
1878
+ })
1879
+ .setNegativeButton(shareDisclaimer.getString("cancelBtn", "Cancel"), null)
1880
+ .show();
1881
+ } else {
1882
+ shareUrl();
1883
+ }
1884
+ });
1885
+ } else {
1886
+ shareButton.setVisibility(View.GONE);
1887
+ }
1888
+
1889
+ // Also color the title text
1890
+ TextView titleText = _toolbar.findViewById(R.id.titleText);
1891
+ if (titleText != null) {
1892
+ titleText.setTextColor(iconColor);
1893
+
1894
+ // Set the title text
1895
+ if (!TextUtils.isEmpty(_options.getTitle())) {
1896
+ this.setTitle(_options.getTitle());
1897
+ } else {
1898
+ try {
1899
+ URI uri = new URI(_options.getUrl());
1900
+ this.setTitle(uri.getHost());
1901
+ } catch (URISyntaxException e) {
1902
+ this.setTitle(_options.getTitle());
1903
+ }
1904
+ }
1905
+ }
1906
+ }
1907
+
1908
+ /**
1909
+ * Applies background and tint colors to all buttons in the toolbar
1910
+ */
1911
+ private void applyColorToAllButtons(int backgroundColor, int iconColor) {
1912
+ // Get all buttons
1913
+ ImageButton backButton = _toolbar.findViewById(R.id.backButton);
1914
+ ImageButton forwardButton = _toolbar.findViewById(R.id.forwardButton);
1915
+ ImageButton closeButton = _toolbar.findViewById(R.id.closeButton);
1916
+ ImageButton reloadButton = _toolbar.findViewById(R.id.reloadButton);
1917
+ ImageButton shareButton = _toolbar.findViewById(R.id.shareButton);
1918
+ ImageButton buttonNearDoneView = _toolbar.findViewById(R.id.buttonNearDone);
1919
+
1920
+ // Set button backgrounds
1921
+ backButton.setBackgroundColor(backgroundColor);
1922
+ forwardButton.setBackgroundColor(backgroundColor);
1923
+ closeButton.setBackgroundColor(backgroundColor);
1924
+ reloadButton.setBackgroundColor(backgroundColor);
1925
+
1926
+ // Apply tint colors to buttons
1927
+ backButton.setColorFilter(iconColor);
1928
+ forwardButton.setColorFilter(iconColor);
1929
+ closeButton.setColorFilter(iconColor);
1930
+ reloadButton.setColorFilter(iconColor);
1931
+ shareButton.setColorFilter(iconColor);
1932
+ buttonNearDoneView.setColorFilter(iconColor);
1933
+ }
1934
+
1935
+ public void handleProxyResultError(String result, String id) {
1936
+ Log.i("InAppBrowserProxy", String.format("handleProxyResultError: %s, ok: %s id: %s", result, false, id));
1937
+ ProxiedRequest proxiedRequest = proxiedRequestsHashmap.get(id);
1938
+ if (proxiedRequest == null) {
1939
+ Log.e("InAppBrowserProxy", "proxiedRequest is null");
1940
+ return;
1941
+ }
1942
+ proxiedRequestsHashmap.remove(id);
1943
+ proxiedRequest.semaphore.release();
1944
+ }
1945
+
1946
+ public void handleProxyResultOk(JSONObject result, String id) {
1947
+ Log.i("InAppBrowserProxy", String.format("handleProxyResultOk: %s, ok: %s, id: %s", result, true, id));
1948
+ ProxiedRequest proxiedRequest = proxiedRequestsHashmap.get(id);
1949
+ if (proxiedRequest == null) {
1950
+ Log.e("InAppBrowserProxy", "proxiedRequest is null");
1951
+ return;
1952
+ }
1953
+ proxiedRequestsHashmap.remove(id);
1954
+
1955
+ if (result == null) {
1956
+ proxiedRequest.semaphore.release();
1957
+ return;
1958
+ }
1959
+
1960
+ Map<String, String> responseHeaders = new HashMap<>();
1961
+ String body;
1962
+ int code;
1963
+
1964
+ try {
1965
+ body = result.getString("body");
1966
+ code = result.getInt("code");
1967
+ JSONObject headers = result.getJSONObject("headers");
1968
+ for (Iterator<String> it = headers.keys(); it.hasNext(); ) {
1969
+ String headerName = it.next();
1970
+ String header = headers.getString(headerName);
1971
+ responseHeaders.put(headerName, header);
1972
+ }
1973
+ } catch (JSONException e) {
1974
+ Log.e("InAppBrowserProxy", "Cannot parse OK result", e);
1975
+ return;
1976
+ }
1977
+
1978
+ String contentType = responseHeaders.get("Content-Type");
1979
+ if (contentType == null) {
1980
+ contentType = responseHeaders.get("content-type");
1981
+ }
1982
+ if (contentType == null) {
1983
+ Log.e("InAppBrowserProxy", "'Content-Type' header is required");
1984
+ return;
1985
+ }
1986
+
1987
+ if (!((100 <= code && code <= 299) || (400 <= code && code <= 599))) {
1988
+ Log.e("InAppBrowserProxy", String.format("Status code %s outside of the allowed range", code));
1989
+ return;
1990
+ }
1991
+
1992
+ WebResourceResponse webResourceResponse = new WebResourceResponse(
1993
+ contentType,
1994
+ "utf-8",
1995
+ new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))
1996
+ );
1997
+
1998
+ webResourceResponse.setStatusCodeAndReasonPhrase(code, getReasonPhrase(code));
1999
+ proxiedRequest.response = webResourceResponse;
2000
+ proxiedRequest.semaphore.release();
2001
+ }
2002
+
2003
+ private void setWebViewClient() {
2004
+ _webView.setWebViewClient(
2005
+ new WebViewClient() {
2006
+ /**
2007
+ * Checks whether the given URL is authorized based on the provided list of authorized links.
2008
+ * <p>
2009
+ * For http(s) URLs, compares only the host (ignoring "www." prefix and case).
2010
+ * Each entry in authorizedLinks should be a base URL (e.g., "https://example.com").
2011
+ * If the host of the input URL matches (case-insensitive) the host of any authorized link, returns true.
2012
+ * <p>
2013
+ * This method is intended to limit which external links can be handled as authorized app links.
2014
+ *
2015
+ * @param url The URL to check. Can be any valid absolute URL.
2016
+ * @param authorizedLinks List of authorized base URLs (e.g., "https://mydomain.com", "myapp://").
2017
+ * @return true if the URL is authorized (host matches one of the authorizedLinks); false otherwise.
2018
+ */
2019
+ private boolean isUrlAuthorized(String url, List<String> authorizedLinks) {
2020
+ if (authorizedLinks == null || authorizedLinks.isEmpty() || url == null) {
2021
+ return false;
2022
+ }
2023
+ try {
2024
+ URI uri = new URI(url);
2025
+ String urlHost = uri.getHost();
2026
+ if (urlHost == null) return false;
2027
+ if (urlHost.startsWith("www.")) urlHost = urlHost.substring(4);
2028
+ for (String authorized : authorizedLinks) {
2029
+ URI authUri = new URI(authorized);
2030
+ String authHost = authUri.getHost();
2031
+ if (authHost == null) continue;
2032
+ if (authHost.startsWith("www.")) authHost = authHost.substring(4);
2033
+ if (urlHost.equalsIgnoreCase(authHost)) {
2034
+ return true;
2035
+ }
2036
+ }
2037
+ } catch (URISyntaxException e) {
2038
+ Log.e("InAppBrowser", "Invalid URI in isUrlAuthorized: " + url, e);
2039
+ }
2040
+ return false;
2041
+ }
2042
+
2043
+ /**
2044
+ * Checks if a host should be blocked based on the configured blocked hosts patterns
2045
+ * @param url The URL to check
2046
+ * @param blockedHosts The list of blocked hosts patterns
2047
+ * @return true if the host should be blocked, false otherwise
2048
+ */
2049
+ private boolean shouldBlockHost(String url, List<String> blockedHosts) {
2050
+ Uri uri = Uri.parse(url);
2051
+ String host = uri.getHost();
2052
+
2053
+ if (host == null || host.isEmpty()) {
2054
+ return false;
2055
+ }
2056
+
2057
+ if (blockedHosts == null || blockedHosts.isEmpty()) {
2058
+ return false;
2059
+ }
2060
+
2061
+ String normalizedHost = host.toLowerCase();
2062
+
2063
+ for (String blockPattern : blockedHosts) {
2064
+ if (blockPattern != null && matchesBlockPattern(normalizedHost, blockPattern.toLowerCase())) {
2065
+ Log.d("InAppBrowser", "Blocked host detected: " + host);
2066
+ return true;
2067
+ }
2068
+ }
2069
+
2070
+ return false;
2071
+ }
2072
+
2073
+ /**
2074
+ * Matches a host against a blocking pattern (supports wildcards)
2075
+ * @param host The normalized host to check
2076
+ * @param pattern The normalized blocking pattern
2077
+ * @return true if the host matches the pattern
2078
+ */
2079
+ private boolean matchesBlockPattern(String host, String pattern) {
2080
+ if (pattern == null || pattern.isEmpty()) {
2081
+ return false;
2082
+ }
2083
+
2084
+ // Exact match - fastest check first
2085
+ if (host.equals(pattern)) {
2086
+ return true;
2087
+ }
2088
+
2089
+ // No wildcards - already checked exact match above
2090
+ if (!pattern.contains("*")) {
2091
+ return false;
2092
+ }
2093
+
2094
+ // Handle wildcard patterns
2095
+ if (pattern.startsWith("*.")) {
2096
+ return matchesWildcardDomain(host, pattern);
2097
+ } else if (pattern.contains("*")) {
2098
+ return matchesRegexPattern(host, pattern);
2099
+ }
2100
+
2101
+ return false;
2102
+ }
2103
+
2104
+ /**
2105
+ * Handles simple subdomain wildcard patterns like "*.example.com"
2106
+ * @param host The host to check
2107
+ * @param pattern The wildcard pattern starting with "*."
2108
+ * @return true if the host matches the wildcard domain
2109
+ */
2110
+ private boolean matchesWildcardDomain(String host, String pattern) {
2111
+ String domain = pattern.substring(2); // Remove "*."
2112
+
2113
+ if (domain.isEmpty()) {
2114
+ return false;
2115
+ }
2116
+
2117
+ // Match exact domain or any subdomain
2118
+ return host.equals(domain) || host.endsWith("." + domain);
2119
+ }
2120
+
2121
+ /**
2122
+ * Handles complex regex patterns with multiple wildcards
2123
+ * @param host The host to check
2124
+ * @param pattern The pattern with wildcards to convert to regex
2125
+ * @return true if the host matches the regex pattern
2126
+ */
2127
+ private boolean matchesRegexPattern(String host, String pattern) {
2128
+ try {
2129
+ // Escape special regex characters except *
2130
+ String escapedPattern = pattern
2131
+ .replace("\\", "\\\\") // Must escape backslashes first
2132
+ .replace(".", "\\.")
2133
+ .replace("+", "\\+")
2134
+ .replace("?", "\\?")
2135
+ .replace("^", "\\^")
2136
+ .replace("$", "\\$")
2137
+ .replace("(", "\\(")
2138
+ .replace(")", "\\)")
2139
+ .replace("[", "\\[")
2140
+ .replace("]", "\\]")
2141
+ .replace("{", "\\{")
2142
+ .replace("}", "\\}")
2143
+ .replace("|", "\\|");
2144
+
2145
+ // Convert wildcards to regex
2146
+ String regexPattern = "^" + escapedPattern.replace("*", ".*") + "$";
2147
+
2148
+ return Pattern.matches(regexPattern, host);
2149
+ } catch (Exception e) {
2150
+ Log.e("InAppBrowser", "Invalid regex pattern '" + pattern + "': " + e.getMessage());
2151
+ return false;
2152
+ }
2153
+ }
2154
+
2155
+ @Override
2156
+ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
2157
+ if (view == null || _webView == null) {
2158
+ return false;
2159
+ }
2160
+ Context context = view.getContext();
2161
+ String url = request.getUrl().toString();
2162
+ Log.d("InAppBrowser", "shouldOverrideUrlLoading: " + url);
2163
+
2164
+ boolean isNotHttpOrHttps = !url.startsWith("https://") && !url.startsWith("http://");
2165
+
2166
+ // If preventDeeplink is true, don't handle any non-http(s) URLs
2167
+ if (_options.getPreventDeeplink()) {
2168
+ Log.d("InAppBrowser", "preventDeeplink is true");
2169
+ if (isNotHttpOrHttps) {
2170
+ return true;
2171
+ }
2172
+ }
2173
+
2174
+ // Handle authorized app links
2175
+ List<String> authorizedLinks = _options.getAuthorizedAppLinks();
2176
+ boolean urlAuthorized = isUrlAuthorized(url, authorizedLinks);
2177
+
2178
+ Log.d("InAppBrowser", "authorizedLinks: " + authorizedLinks);
2179
+ Log.d("InAppBrowser", "urlAuthorized: " + urlAuthorized);
2180
+
2181
+ if (urlAuthorized) {
2182
+ try {
2183
+ Log.d("InAppBrowser", "Launching intent for authorized link: " + url);
2184
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
2185
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2186
+ context.startActivity(intent);
2187
+ Log.i("InAppBrowser", "Intent started for authorized link: " + url);
2188
+ return true;
2189
+ } catch (ActivityNotFoundException e) {
2190
+ Log.e("InAppBrowser", "No app found to handle this authorized link", e);
2191
+ return false;
2192
+ }
2193
+ }
2194
+
2195
+ if (isNotHttpOrHttps) {
2196
+ try {
2197
+ Intent intent;
2198
+ if (url.startsWith("intent://")) {
2199
+ intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
2200
+ } else {
2201
+ intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
2202
+ }
2203
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2204
+ context.startActivity(intent);
2205
+ return true;
2206
+ } catch (ActivityNotFoundException | URISyntaxException e) {
2207
+ Log.w("InAppBrowser", "No handler for external URL: " + url, e);
2208
+ // Notify that a page load error occurred
2209
+ if (_options.getCallbacks() != null && request.isForMainFrame()) {
2210
+ _options.getCallbacks().pageLoadError();
2211
+ }
2212
+ return true; // prevent WebView from attempting to load the custom scheme
2213
+ }
2214
+ }
2215
+
2216
+ // Check for blocked hosts (main-frame only) using the extracted function
2217
+ List<String> blockedHosts = _options.getBlockedHosts();
2218
+ if (blockedHosts != null && !blockedHosts.isEmpty() && request.isForMainFrame()) {
2219
+ Log.d("InAppBrowser", "Checking for blocked hosts (on main frame)");
2220
+ if (shouldBlockHost(url, blockedHosts)) {
2221
+ // Make sure to notify that a URL has changed even when it was blocked
2222
+ if (_options.getCallbacks() != null) {
2223
+ _options.getCallbacks().urlChangeEvent(url);
2224
+ }
2225
+ Log.d("InAppBrowser", "Navigation blocked for URL: " + url);
2226
+ return true; // Block the navigation
2227
+ }
2228
+ }
2229
+
2230
+ return false;
2231
+ }
2232
+
2233
+ @Override
2234
+ public void onReceivedClientCertRequest(WebView view, android.webkit.ClientCertRequest request) {
2235
+ Log.i("InAppBrowser", "onReceivedClientCertRequest CALLED");
2236
+
2237
+ if (request == null) {
2238
+ Log.e("InAppBrowser", "ClientCertRequest is null");
2239
+ return;
2240
+ }
2241
+
2242
+ if (activity == null) {
2243
+ Log.e("InAppBrowser", "Activity is null, canceling request");
2244
+ try {
2245
+ request.cancel();
2246
+ } catch (Exception e) {
2247
+ Log.e("InAppBrowser", "Error canceling request: " + e.getMessage());
2248
+ }
2249
+ return;
2250
+ }
2251
+
2252
+ try {
2253
+ Log.i("InAppBrowser", "Host: " + request.getHost());
2254
+ Log.i("InAppBrowser", "Port: " + request.getPort());
2255
+ Log.i("InAppBrowser", "Principals: " + java.util.Arrays.toString(request.getPrincipals()));
2256
+ Log.i("InAppBrowser", "KeyTypes: " + java.util.Arrays.toString(request.getKeyTypes()));
2257
+
2258
+ KeyChain.choosePrivateKeyAlias(
2259
+ activity,
2260
+ new KeyChainAliasCallback() {
2261
+ @Override
2262
+ public void alias(String alias) {
2263
+ if (alias != null) {
2264
+ try {
2265
+ PrivateKey privateKey = KeyChain.getPrivateKey(activity, alias);
2266
+ X509Certificate[] certChain = KeyChain.getCertificateChain(activity, alias);
2267
+ request.proceed(privateKey, certChain);
2268
+ Log.i("InAppBrowser", "Selected certificate: " + alias);
2269
+ } catch (Exception e) {
2270
+ try {
2271
+ request.cancel();
2272
+ } catch (Exception cancelEx) {
2273
+ Log.e("InAppBrowser", "Error canceling request: " + cancelEx.getMessage());
2274
+ }
2275
+ Log.e("InAppBrowser", "Error selecting certificate: " + e.getMessage());
2276
+ }
2277
+ } else {
2278
+ try {
2279
+ request.cancel();
2280
+ } catch (Exception e) {
2281
+ Log.e("InAppBrowser", "Error canceling request: " + e.getMessage());
2282
+ }
2283
+ Log.i("InAppBrowser", "No certificate found");
2284
+ }
2285
+ }
2286
+ },
2287
+ null, // keyTypes
2288
+ null, // issuers
2289
+ request.getHost(),
2290
+ request.getPort(),
2291
+ null // alias (null = system asks user to choose)
2292
+ );
2293
+ } catch (Exception e) {
2294
+ Log.e("InAppBrowser", "Error in onReceivedClientCertRequest: " + e.getMessage());
2295
+ try {
2296
+ request.cancel();
2297
+ } catch (Exception cancelEx) {
2298
+ Log.e("InAppBrowser", "Error canceling request after exception: " + cancelEx.getMessage());
2299
+ }
2300
+ }
2301
+ }
2302
+
2303
+ private String randomRequestId() {
2304
+ return UUID.randomUUID().toString();
2305
+ }
2306
+
2307
+ private String toBase64(String raw) {
2308
+ String s = Base64.encodeToString(raw.getBytes(), Base64.NO_WRAP);
2309
+ if (s.endsWith("=")) {
2310
+ s = s.substring(0, s.length() - 2);
2311
+ }
2312
+ return s;
2313
+ }
2314
+
2315
+ //
2316
+ // void handleRedirect(String currentUrl, Response response) {
2317
+ // String loc = response.header("Location");
2318
+ // _webView.evaluateJavascript("");
2319
+ // }
2320
+ //
2321
+ @Override
2322
+ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
2323
+ if (view == null || _webView == null) {
2324
+ return null;
2325
+ }
2326
+ Pattern pattern = _options.getProxyRequestsPattern();
2327
+ if (pattern == null) {
2328
+ return null;
2329
+ }
2330
+ Matcher matcher = pattern.matcher(request.getUrl().toString());
2331
+ if (!matcher.find()) {
2332
+ return null;
2333
+ }
2334
+
2335
+ // Requests matches the regex
2336
+ if (Objects.equals(request.getMethod(), "POST")) {
2337
+ // Log.e("HTTP", String.format("returned null (ok) %s", request.getUrl().toString()));
2338
+ return null;
2339
+ }
2340
+
2341
+ Log.i("InAppBrowserProxy", String.format("Proxying request: %s", request.getUrl().toString()));
2342
+
2343
+ // We need to call a JS function
2344
+ String requestId = randomRequestId();
2345
+ ProxiedRequest proxiedRequest = new ProxiedRequest();
2346
+ addProxiedRequest(requestId, proxiedRequest);
2347
+
2348
+ // lsuakdchgbbaHandleProxiedRequest
2349
+ activity.runOnUiThread(
2350
+ new Runnable() {
2351
+ @Override
2352
+ public void run() {
2353
+ StringBuilder headers = new StringBuilder();
2354
+ Map<String, String> requestHeaders = request.getRequestHeaders();
2355
+ for (Map.Entry<String, String> header : requestHeaders.entrySet()) {
2356
+ headers.append(
2357
+ String.format("h[atob('%s')]=atob('%s');", toBase64(header.getKey()), toBase64(header.getValue()))
2358
+ );
2359
+ }
2360
+ String jsTemplate = """
2361
+ try {
2362
+ function getHeaders() {
2363
+ const h = {};
2364
+ %s
2365
+ return h;
2366
+ }
2367
+ window.InAppBrowserProxyRequest(new Request(atob('%s'), {
2368
+ headers: getHeaders(),
2369
+ method: '%s'
2370
+ })).then(async (res) => {
2371
+ Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({
2372
+ ok: true,
2373
+ result: (!!res ? {
2374
+ headers: Object.fromEntries(res.headers.entries()),
2375
+ code: res.status,
2376
+ body: (await res.text())
2377
+ } : null),
2378
+ id: '%s'
2379
+ });
2380
+ }).catch((e) => {
2381
+ Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({
2382
+ ok: false,
2383
+ result: e.toString(),
2384
+ id: '%s'
2385
+ });
2386
+ });
2387
+ } catch (e) {
2388
+ Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({
2389
+ ok: false,
2390
+ result: e.toString(),
2391
+ id: '%s'
2392
+ });
2393
+ }
2394
+ """;
2395
+ String s = String.format(
2396
+ jsTemplate,
2397
+ headers,
2398
+ toBase64(request.getUrl().toString()),
2399
+ request.getMethod(),
2400
+ requestId,
2401
+ requestId,
2402
+ requestId
2403
+ );
2404
+ // Log.i("HTTP", s);
2405
+ capacitorWebView.evaluateJavascript(s, null);
2406
+ }
2407
+ }
2408
+ );
2409
+
2410
+ // 10 seconds wait max
2411
+ try {
2412
+ if (proxiedRequest.semaphore.tryAcquire(1, 10, TimeUnit.SECONDS)) {
2413
+ return proxiedRequest.response;
2414
+ } else {
2415
+ Log.e("InAppBrowserProxy", "Semaphore timed out");
2416
+ removeProxiedRequest(requestId); // prevent mem leak
2417
+ }
2418
+ } catch (InterruptedException e) {
2419
+ Log.e("InAppBrowserProxy", "Semaphore wait error", e);
2420
+ }
2421
+ return null;
2422
+ }
2423
+
2424
+ @Override
2425
+ public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
2426
+ if (view == null || _webView == null) {
2427
+ if (handler != null) {
2428
+ handler.cancel();
2429
+ }
2430
+ return;
2431
+ }
2432
+ final String sourceUrl = _options.getUrl();
2433
+ final String url = view.getUrl();
2434
+ final JSObject credentials = _options.getCredentials();
2435
+
2436
+ if (
2437
+ credentials != null &&
2438
+ credentials.getString("username") != null &&
2439
+ credentials.getString("password") != null &&
2440
+ sourceUrl != null &&
2441
+ url != null
2442
+ ) {
2443
+ String sourceProtocol = "";
2444
+ String sourceHost = "";
2445
+ int sourcePort = -1;
2446
+ try {
2447
+ URI uri = new URI(sourceUrl);
2448
+ sourceProtocol = uri.getScheme();
2449
+ sourceHost = uri.getHost();
2450
+ sourcePort = uri.getPort();
2451
+ if (sourcePort == -1 && Objects.equals(sourceProtocol, "https")) sourcePort = 443;
2452
+ else if (sourcePort == -1 && Objects.equals(sourceProtocol, "http")) sourcePort = 80;
2453
+ } catch (URISyntaxException e) {
2454
+ e.printStackTrace();
2455
+ }
2456
+
2457
+ String protocol = "";
2458
+ int port = -1;
2459
+ try {
2460
+ URI uri = new URI(url);
2461
+ protocol = uri.getScheme();
2462
+ port = uri.getPort();
2463
+ if (port == -1 && Objects.equals(protocol, "https")) port = 443;
2464
+ else if (port == -1 && Objects.equals(protocol, "http")) port = 80;
2465
+ } catch (URISyntaxException e) {
2466
+ e.printStackTrace();
2467
+ }
2468
+
2469
+ if (Objects.equals(sourceHost, host) && Objects.equals(sourceProtocol, protocol) && sourcePort == port) {
2470
+ final String username = Objects.requireNonNull(credentials.getString("username"));
2471
+ final String password = Objects.requireNonNull(credentials.getString("password"));
2472
+ handler.proceed(username, password);
2473
+ return;
2474
+ }
2475
+ }
2476
+
2477
+ super.onReceivedHttpAuthRequest(view, handler, host, realm);
2478
+ }
2479
+
2480
+ @Override
2481
+ public void onLoadResource(WebView view, String url) {
2482
+ if (view == null || _webView == null) {
2483
+ return;
2484
+ }
2485
+ super.onLoadResource(view, url);
2486
+ }
2487
+
2488
+ @Override
2489
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
2490
+ super.onPageStarted(view, url, favicon);
2491
+ if (view == null || _webView == null) {
2492
+ return;
2493
+ }
2494
+ try {
2495
+ URI uri = new URI(url);
2496
+ if (TextUtils.isEmpty(_options.getTitle())) {
2497
+ setTitle(uri.getHost());
2498
+ }
2499
+ } catch (URISyntaxException e) {
2500
+ // Do nothing
2501
+ }
2502
+ }
2503
+
2504
+ public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
2505
+ if (view == null || _webView == null) {
2506
+ return;
2507
+ }
2508
+ if (!isReload) {
2509
+ _options.getCallbacks().urlChangeEvent(url);
2510
+ }
2511
+ super.doUpdateVisitedHistory(view, url, isReload);
2512
+ injectJavaScriptInterface();
2513
+
2514
+ // Inject Google Pay polyfills if enabled
2515
+ if (_options.getEnableGooglePaySupport()) {
2516
+ injectGooglePayPolyfills();
2517
+ }
2518
+ }
2519
+
2520
+ @Override
2521
+ public void onPageFinished(WebView view, String url) {
2522
+ super.onPageFinished(view, url);
2523
+ if (view == null || _webView == null) {
2524
+ return;
2525
+ }
2526
+ if (!isInitialized) {
2527
+ isInitialized = true;
2528
+ _webView.clearHistory();
2529
+ if (_options.isPresentAfterPageLoad()) {
2530
+ boolean usePreShowScript = _options.getPreShowScript() != null && !_options.getPreShowScript().isEmpty();
2531
+ if (!usePreShowScript) {
2532
+ show();
2533
+ _options.getPluginCall().resolve();
2534
+ } else {
2535
+ executorService.execute(
2536
+ new Runnable() {
2537
+ @Override
2538
+ public void run() {
2539
+ if (_options.getPreShowScript() != null && !_options.getPreShowScript().isEmpty()) {
2540
+ injectPreShowScript();
2541
+ }
2542
+
2543
+ activity.runOnUiThread(
2544
+ new Runnable() {
2545
+ @Override
2546
+ public void run() {
2547
+ show();
2548
+ _options.getPluginCall().resolve();
2549
+ }
2550
+ }
2551
+ );
2552
+ }
2553
+ }
2554
+ );
2555
+ }
2556
+ }
2557
+ } else if (_options.getPreShowScript() != null && !_options.getPreShowScript().isEmpty()) {
2558
+ executorService.execute(
2559
+ new Runnable() {
2560
+ @Override
2561
+ public void run() {
2562
+ injectPreShowScript();
2563
+ }
2564
+ }
2565
+ );
2566
+ }
2567
+
2568
+ ImageButton backButton = _toolbar.findViewById(R.id.backButton);
2569
+ if (_webView != null && _webView.canGoBack()) {
2570
+ backButton.setImageResource(R.drawable.arrow_back_enabled);
2571
+ backButton.setEnabled(true);
2572
+ backButton.setColorFilter(iconColor);
2573
+ backButton.setOnClickListener(
2574
+ new View.OnClickListener() {
2575
+ @Override
2576
+ public void onClick(View view) {
2577
+ if (_webView != null && _webView.canGoBack()) {
2578
+ _webView.goBack();
2579
+ }
2580
+ }
2581
+ }
2582
+ );
2583
+ } else {
2584
+ backButton.setImageResource(R.drawable.arrow_back_disabled);
2585
+ backButton.setEnabled(false);
2586
+ backButton.setColorFilter(Color.argb(128, Color.red(iconColor), Color.green(iconColor), Color.blue(iconColor)));
2587
+ }
2588
+
2589
+ ImageButton forwardButton = _toolbar.findViewById(R.id.forwardButton);
2590
+ if (_webView != null && _webView.canGoForward()) {
2591
+ forwardButton.setImageResource(R.drawable.arrow_forward_enabled);
2592
+ forwardButton.setEnabled(true);
2593
+ forwardButton.setColorFilter(iconColor);
2594
+ forwardButton.setOnClickListener(
2595
+ new View.OnClickListener() {
2596
+ @Override
2597
+ public void onClick(View view) {
2598
+ if (_webView != null && _webView.canGoForward()) {
2599
+ _webView.goForward();
2600
+ }
2601
+ }
2602
+ }
2603
+ );
2604
+ } else {
2605
+ forwardButton.setImageResource(R.drawable.arrow_forward_disabled);
2606
+ forwardButton.setEnabled(false);
2607
+ forwardButton.setColorFilter(Color.argb(128, Color.red(iconColor), Color.green(iconColor), Color.blue(iconColor)));
2608
+ }
2609
+
2610
+ _options.getCallbacks().pageLoaded();
2611
+ injectJavaScriptInterface();
2612
+
2613
+ // Inject Google Pay polyfills if enabled
2614
+ if (_options.getEnableGooglePaySupport()) {
2615
+ injectGooglePayPolyfills();
2616
+ }
2617
+ }
2618
+
2619
+ @Override
2620
+ public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
2621
+ super.onReceivedError(view, request, error);
2622
+ if (view == null || _webView == null) {
2623
+ return;
2624
+ }
2625
+ _options.getCallbacks().pageLoadError();
2626
+ }
2627
+
2628
+ @SuppressLint("WebViewClientOnReceivedSslError")
2629
+ @Override
2630
+ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
2631
+ if (view == null || _webView == null) {
2632
+ if (handler != null) {
2633
+ handler.cancel();
2634
+ }
2635
+ return;
2636
+ }
2637
+ boolean ignoreSSLUntrustedError = _options.ignoreUntrustedSSLError();
2638
+ if (ignoreSSLUntrustedError && error.getPrimaryError() == SslError.SSL_UNTRUSTED) handler.proceed();
2639
+ else {
2640
+ super.onReceivedSslError(view, handler, error);
2641
+ }
2642
+ }
2643
+ }
2644
+ );
2645
+ }
2646
+
2647
+ /**
2648
+ * Navigates back in the WebView history if possible
2649
+ * @return true if navigation was successful, false otherwise
2650
+ */
2651
+ public boolean goBack() {
2652
+ if (_webView != null && _webView.canGoBack()) {
133
2653
  _webView.goBack();
134
- }
135
- }
136
- }
137
- );
138
-
139
- View forwardButton = _toolbar.findViewById(R.id.forwardButton);
140
- forwardButton.setOnClickListener(
141
- new View.OnClickListener() {
142
- @Override
143
- public void onClick(View view) {
144
- if (_webView.canGoForward()) {
145
- _webView.goForward();
146
- }
147
- }
148
- }
149
- );
150
-
151
- View closeButton = _toolbar.findViewById(R.id.closeButton);
152
- closeButton.setOnClickListener(
153
- new View.OnClickListener() {
154
- @Override
155
- public void onClick(View view) {
156
- dismiss();
157
- _options.getCallbacks().closeEvent(_webView.getUrl());
158
- }
159
- }
160
- );
161
-
162
- if (TextUtils.equals(_options.getToolbarType(), "activity")) {
163
- _toolbar.findViewById(R.id.forwardButton).setVisibility(View.GONE);
164
- _toolbar.findViewById(R.id.backButton).setVisibility(View.GONE);
165
- //TODO: Add share button functionality
166
- } else if (TextUtils.equals(_options.getToolbarType(), "navigation")) {
167
- //TODO: Remove share button when implemented
168
- } else if (TextUtils.equals(_options.getToolbarType(), "blank")) {
169
- _toolbar.setVisibility(View.GONE);
170
- } else {
171
- _toolbar.findViewById(R.id.forwardButton).setVisibility(View.GONE);
172
- _toolbar.findViewById(R.id.backButton).setVisibility(View.GONE);
173
- }
174
- }
175
-
176
- private void setWebViewClient() {
177
- _webView.setWebViewClient(
178
- new WebViewClient() {
179
- @Override
180
- public boolean shouldOverrideUrlLoading(
181
- WebView view,
182
- WebResourceRequest request
183
- ) {
184
- return false;
185
- }
186
-
187
- @Override
188
- public void onLoadResource(WebView view, String url) {
189
- super.onLoadResource(view, url);
190
- }
191
-
192
- @Override
193
- public void onPageStarted(WebView view, String url, Bitmap favicon) {
194
- super.onPageStarted(view, url, favicon);
195
- try {
196
- URI uri = new URI(url);
197
- setTitle(uri.getHost());
198
- } catch (URISyntaxException e) {
199
- // Do nothing
200
- }
201
- _options.getCallbacks().urlChangeEvent(url);
202
- }
203
-
204
- @Override
205
- public void onPageFinished(WebView view, String url) {
206
- super.onPageFinished(view, url);
207
- _options.getCallbacks().pageLoaded();
208
- if (!isInitialized) {
209
- isInitialized = true;
210
- _webView.clearHistory();
211
- if (_options.isPresentAfterPageLoad()) {
212
- show();
213
- _options.getPluginCall().resolve();
214
- }
215
- }
216
-
217
- ImageButton backButton = _toolbar.findViewById(R.id.backButton);
218
- if (_webView.canGoBack()) {
219
- backButton.setImageResource(R.drawable.arrow_back_enabled);
220
- backButton.setEnabled(true);
221
- } else {
222
- backButton.setImageResource(R.drawable.arrow_back_disabled);
223
- backButton.setEnabled(false);
224
- }
225
-
226
- ImageButton forwardButton = _toolbar.findViewById(R.id.forwardButton);
227
- if (_webView.canGoForward()) {
228
- forwardButton.setImageResource(R.drawable.arrow_forward_enabled);
229
- forwardButton.setEnabled(true);
230
- } else {
231
- forwardButton.setImageResource(R.drawable.arrow_forward_disabled);
232
- forwardButton.setEnabled(false);
233
- }
234
-
235
- _options.getCallbacks().pageLoaded();
236
- }
237
-
238
- @Override
239
- public void onReceivedError(
240
- WebView view,
241
- WebResourceRequest request,
242
- WebResourceError error
2654
+ return true;
2655
+ }
2656
+ return false;
2657
+ }
2658
+
2659
+ @Override
2660
+ public void onBackPressed() {
2661
+ if (
2662
+ _webView != null &&
2663
+ _webView.canGoBack() &&
2664
+ (TextUtils.equals(_options.getToolbarType(), "navigation") || _options.getActiveNativeNavigationForWebview())
243
2665
  ) {
244
- super.onReceivedError(view, request, error);
245
- _options.getCallbacks().pageLoadError();
246
- }
247
- }
248
- );
249
- }
250
-
251
- @Override
252
- public void onBackPressed() {
253
- if (
254
- _webView.canGoBack() &&
255
- TextUtils.equals(_options.getToolbarType(), "navigation")
256
- ) {
257
- _webView.goBack();
258
- } else {
259
- super.onBackPressed();
260
- }
261
- }
2666
+ _webView.goBack();
2667
+ } else if (!_options.getDisableGoBackOnNativeApplication()) {
2668
+ String currentUrl = getUrl();
2669
+ _options.getCallbacks().closeEvent(currentUrl);
2670
+ if (_webView != null) {
2671
+ _webView.destroy();
2672
+ }
2673
+ super.onBackPressed();
2674
+ }
2675
+ }
2676
+
2677
+ @Override
2678
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
2679
+ // Forward volume key events to the MainActivity
2680
+ switch (keyCode) {
2681
+ case KeyEvent.KEYCODE_VOLUME_UP:
2682
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
2683
+ return activity.onKeyDown(keyCode, event);
2684
+ }
2685
+ return super.onKeyDown(keyCode, event);
2686
+ }
2687
+
2688
+ @Override
2689
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
2690
+ // Forward volume key events to the MainActivity
2691
+ switch (keyCode) {
2692
+ case KeyEvent.KEYCODE_VOLUME_UP:
2693
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
2694
+ return activity.onKeyUp(keyCode, event);
2695
+ }
2696
+ return super.onKeyUp(keyCode, event);
2697
+ }
2698
+
2699
+ public static String getReasonPhrase(int statusCode) {
2700
+ return switch (statusCode) {
2701
+ case (200) -> "OK";
2702
+ case (201) -> "Created";
2703
+ case (202) -> "Accepted";
2704
+ case (203) -> "Non Authoritative Information";
2705
+ case (204) -> "No Content";
2706
+ case (205) -> "Reset Content";
2707
+ case (206) -> "Partial Content";
2708
+ case (207) -> "Partial Update OK";
2709
+ case (300) -> "Multiple Choices";
2710
+ case (301) -> "Moved Permanently";
2711
+ case (302) -> "Moved Temporarily";
2712
+ case (303) -> "See Other";
2713
+ case (304) -> "Not Modified";
2714
+ case (305) -> "Use Proxy";
2715
+ case (307) -> "Temporary Redirect";
2716
+ case (400) -> "Bad Request";
2717
+ case (401) -> "Unauthorized";
2718
+ case (402) -> "Payment Required";
2719
+ case (403) -> "Forbidden";
2720
+ case (404) -> "Not Found";
2721
+ case (405) -> "Method Not Allowed";
2722
+ case (406) -> "Not Acceptable";
2723
+ case (407) -> "Proxy Authentication Required";
2724
+ case (408) -> "Request Timeout";
2725
+ case (409) -> "Conflict";
2726
+ case (410) -> "Gone";
2727
+ case (411) -> "Length Required";
2728
+ case (412) -> "Precondition Failed";
2729
+ case (413) -> "Request Entity Too Large";
2730
+ case (414) -> "Request-URI Too Long";
2731
+ case (415) -> "Unsupported Media Type";
2732
+ case (416) -> "Requested Range Not Satisfiable";
2733
+ case (417) -> "Expectation Failed";
2734
+ case (418) -> "Reauthentication Required";
2735
+ case (419) -> "Proxy Reauthentication Required";
2736
+ case (422) -> "Unprocessable Entity";
2737
+ case (423) -> "Locked";
2738
+ case (424) -> "Failed Dependency";
2739
+ case (500) -> "Server Error";
2740
+ case (501) -> "Not Implemented";
2741
+ case (502) -> "Bad Gateway";
2742
+ case (503) -> "Service Unavailable";
2743
+ case (504) -> "Gateway Timeout";
2744
+ case (505) -> "HTTP Version Not Supported";
2745
+ case (507) -> "Insufficient Storage";
2746
+ default -> "";
2747
+ };
2748
+ }
2749
+
2750
+ @Override
2751
+ public void dismiss() {
2752
+ // First, stop any ongoing operations and disable further interactions
2753
+ if (_webView != null) {
2754
+ try {
2755
+ // Stop loading first to prevent any ongoing operations
2756
+ _webView.stopLoading();
2757
+
2758
+ // Clear any pending callbacks to prevent memory leaks
2759
+ if (mFilePathCallback != null) {
2760
+ mFilePathCallback.onReceiveValue(null);
2761
+ mFilePathCallback = null;
2762
+ }
2763
+ tempCameraUri = null;
2764
+
2765
+ // Clear file inputs for security/privacy before destroying WebView
2766
+ try {
2767
+ String clearInputsScript = """
2768
+ (function() {
2769
+ try {
2770
+ var inputs = document.querySelectorAll('input[type="file"]');
2771
+ for (var i = 0; i < inputs.length; i++) {
2772
+ inputs[i].value = '';
2773
+ }
2774
+ return true;
2775
+ } catch(e) {
2776
+ console.log('Error clearing file inputs:', e);
2777
+ return false;
2778
+ }
2779
+ })();
2780
+ """;
2781
+ _webView.evaluateJavascript(clearInputsScript, null);
2782
+ } catch (Exception e) {
2783
+ Log.w("InAppBrowser", "Could not clear file inputs (WebView may be in invalid state): " + e.getMessage());
2784
+ }
2785
+
2786
+ // Remove JavaScript interfaces before destroying
2787
+ _webView.removeJavascriptInterface("AndroidInterface");
2788
+ _webView.removeJavascriptInterface("PreShowScriptInterface");
2789
+ _webView.removeJavascriptInterface("PrintInterface");
2790
+
2791
+ // Load blank page and cleanup
2792
+ _webView.loadUrl("about:blank");
2793
+ _webView.onPause();
2794
+ _webView.removeAllViews();
2795
+ _webView.destroy();
2796
+ _webView = null;
2797
+ } catch (Exception e) {
2798
+ Log.e("InAppBrowser", "Error during WebView cleanup: " + e.getMessage());
2799
+ // Force set to null even if cleanup failed
2800
+ _webView = null;
2801
+ }
2802
+ }
2803
+
2804
+ // Shutdown executor service safely
2805
+ if (executorService != null && !executorService.isShutdown()) {
2806
+ try {
2807
+ executorService.shutdown();
2808
+ if (!executorService.awaitTermination(500, TimeUnit.MILLISECONDS)) {
2809
+ executorService.shutdownNow();
2810
+ }
2811
+ } catch (InterruptedException e) {
2812
+ Thread.currentThread().interrupt();
2813
+ executorService.shutdownNow();
2814
+ } catch (Exception e) {
2815
+ Log.e("InAppBrowser", "Error shutting down executor: " + e.getMessage());
2816
+ }
2817
+ }
2818
+
2819
+ // Clear any remaining proxied requests
2820
+ synchronized (proxiedRequestsHashmap) {
2821
+ proxiedRequestsHashmap.clear();
2822
+ }
2823
+
2824
+ try {
2825
+ super.dismiss();
2826
+ } catch (Exception e) {
2827
+ Log.e("InAppBrowser", "Error dismissing dialog: " + e.getMessage());
2828
+ }
2829
+ }
2830
+
2831
+ public void addProxiedRequest(String key, ProxiedRequest request) {
2832
+ synchronized (proxiedRequestsHashmap) {
2833
+ proxiedRequestsHashmap.put(key, request);
2834
+ }
2835
+ }
2836
+
2837
+ public ProxiedRequest getProxiedRequest(String key) {
2838
+ synchronized (proxiedRequestsHashmap) {
2839
+ ProxiedRequest request = proxiedRequestsHashmap.get(key);
2840
+ proxiedRequestsHashmap.remove(key);
2841
+ return request;
2842
+ }
2843
+ }
2844
+
2845
+ public void removeProxiedRequest(String key) {
2846
+ synchronized (proxiedRequestsHashmap) {
2847
+ proxiedRequestsHashmap.remove(key);
2848
+ }
2849
+ }
2850
+
2851
+ private void shareUrl() {
2852
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
2853
+ shareIntent.setType("text/plain");
2854
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, _options.getShareSubject());
2855
+ shareIntent.putExtra(Intent.EXTRA_TEXT, _options.getUrl());
2856
+ _context.startActivity(Intent.createChooser(shareIntent, "Share"));
2857
+ }
2858
+
2859
+ private boolean isDarkColor(int color) {
2860
+ int red = Color.red(color);
2861
+ int green = Color.green(color);
2862
+ int blue = Color.blue(color);
2863
+ double luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255.0;
2864
+ return luminance < 0.5;
2865
+ }
2866
+
2867
+ private boolean isDarkThemeEnabled() {
2868
+ // This method checks if dark theme is currently enabled without using Configuration class
2869
+ try {
2870
+ // On Android 10+, check via resources for night mode
2871
+ Resources.Theme theme = _context.getTheme();
2872
+ TypedValue typedValue = new TypedValue();
2873
+
2874
+ if (theme.resolveAttribute(android.R.attr.isLightTheme, typedValue, true)) {
2875
+ // isLightTheme exists - returns true if light, false if dark
2876
+ return typedValue.data != 1;
2877
+ }
2878
+
2879
+ // Fallback method - check background color of window
2880
+ if (theme.resolveAttribute(android.R.attr.windowBackground, typedValue, true)) {
2881
+ int backgroundColor = typedValue.data;
2882
+ return isDarkColor(backgroundColor);
2883
+ }
2884
+ } catch (Exception e) {
2885
+ // Ignore and fallback to light theme
2886
+ }
2887
+ return false;
2888
+ }
2889
+
2890
+ private void injectDatePickerFixes() {
2891
+ if (_webView == null) {
2892
+ Log.w("InAppBrowser", "Cannot inject date picker fixes - WebView is null");
2893
+ return;
2894
+ }
2895
+
2896
+ if (datePickerInjected) {
2897
+ return;
2898
+ }
2899
+
2900
+ datePickerInjected = true;
2901
+
2902
+ // This script adds minimal fixes for date inputs to use Material Design
2903
+ String script = """
2904
+ (function() {
2905
+ try {
2906
+ // Find all date inputs
2907
+ const dateInputs = document.querySelectorAll('input[type="date"]');
2908
+ dateInputs.forEach(input => {
2909
+ // Ensure change events propagate correctly
2910
+ let lastValue = input.value;
2911
+ input.addEventListener('change', () => {
2912
+ try {
2913
+ if (input.value !== lastValue) {
2914
+ lastValue = input.value;
2915
+ // Dispatch an input event to ensure frameworks detect the change
2916
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2917
+ }
2918
+ } catch(e) {
2919
+ console.error('Error in date input change handler:', e);
2920
+ }
2921
+ });
2922
+ });
2923
+ } catch(e) {
2924
+ console.error('Error applying date picker fixes:', e);
2925
+ }
2926
+ })();""";
2927
+
2928
+ // Execute the script in the WebView
2929
+ _webView.post(() -> {
2930
+ if (_webView != null) {
2931
+ try {
2932
+ _webView.evaluateJavascript(script, null);
2933
+ Log.d("InAppBrowser", "Applied minimal date picker fixes");
2934
+ } catch (Exception e) {
2935
+ Log.e("InAppBrowser", "Error injecting date picker fixes: " + e.getMessage());
2936
+ }
2937
+ }
2938
+ });
2939
+ }
2940
+
2941
+ /**
2942
+ * Creates a temporary URI for storing camera capture
2943
+ * @return URI for the temporary file or null if creation failed
2944
+ */
2945
+ private Uri createTempImageUri() {
2946
+ try {
2947
+ String fileName = "capture_" + System.currentTimeMillis() + ".jpg";
2948
+ java.io.File cacheDir = _context.getCacheDir();
2949
+
2950
+ // Make sure cache directory exists
2951
+ if (!cacheDir.exists() && !cacheDir.mkdirs()) {
2952
+ return null;
2953
+ }
2954
+
2955
+ // Create temporary file
2956
+ java.io.File tempFile = new java.io.File(cacheDir, fileName);
2957
+ if (!tempFile.createNewFile()) {
2958
+ return null;
2959
+ }
2960
+
2961
+ // Get content URI through FileProvider
2962
+ try {
2963
+ return androidx.core.content.FileProvider.getUriForFile(_context, _context.getPackageName() + ".fileprovider", tempFile);
2964
+ } catch (IllegalArgumentException e) {
2965
+ // Try using external storage as fallback
2966
+ java.io.File externalCacheDir = _context.getExternalCacheDir();
2967
+ if (externalCacheDir != null) {
2968
+ tempFile = new java.io.File(externalCacheDir, fileName);
2969
+ final boolean newFile = tempFile.createNewFile();
2970
+ if (!newFile) {
2971
+ Log.d("InAppBrowser", "Error creating new file");
2972
+ }
2973
+ return androidx.core.content.FileProvider.getUriForFile(
2974
+ _context,
2975
+ _context.getPackageName() + ".fileprovider",
2976
+ tempFile
2977
+ );
2978
+ }
2979
+ }
2980
+ return null;
2981
+ } catch (Exception e) {
2982
+ return null;
2983
+ }
2984
+ }
2985
+
2986
+ private File createImageFile() throws IOException {
2987
+ // Create an image file name
2988
+ String timeStamp = new java.text.SimpleDateFormat("yyyyMMdd_HHmmss").format(new java.util.Date());
2989
+ String imageFileName = "JPEG_" + timeStamp + "_";
2990
+ File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
2991
+ File image = File.createTempFile(imageFileName /* prefix */, ".jpg" /* suffix */, storageDir /* directory */);
2992
+ return image;
2993
+ }
2994
+
2995
+ /**
2996
+ * Apply dimensions to the webview window
2997
+ */
2998
+ private void applyDimensions() {
2999
+ Integer width = _options.getWidth();
3000
+ Integer height = _options.getHeight();
3001
+ Integer x = _options.getX();
3002
+ Integer y = _options.getY();
3003
+
3004
+ WindowManager.LayoutParams params = getWindow().getAttributes();
3005
+
3006
+ // If both width and height are specified, use custom dimensions
3007
+ if (width != null && height != null) {
3008
+ params.width = (int) getPixels(width);
3009
+ params.height = (int) getPixels(height);
3010
+ params.x = (x != null) ? (int) getPixels(x) : 0;
3011
+ params.y = (y != null) ? (int) getPixels(y) : 0;
3012
+ } else if (height != null && width == null) {
3013
+ // If only height is specified, use custom height with fullscreen width
3014
+ params.width = WindowManager.LayoutParams.MATCH_PARENT;
3015
+ params.height = (int) getPixels(height);
3016
+ params.x = 0;
3017
+ params.y = (y != null) ? (int) getPixels(y) : 0;
3018
+ } else {
3019
+ // Default to fullscreen
3020
+ params.width = WindowManager.LayoutParams.MATCH_PARENT;
3021
+ params.height = WindowManager.LayoutParams.MATCH_PARENT;
3022
+ params.x = 0;
3023
+ params.y = 0;
3024
+ }
3025
+
3026
+ getWindow().setAttributes(params);
3027
+ }
3028
+
3029
+ /**
3030
+ * Update dimensions at runtime
3031
+ */
3032
+ public void updateDimensions(Integer width, Integer height, Integer x, Integer y) {
3033
+ // Update options
3034
+ if (width != null) {
3035
+ _options.setWidth(width);
3036
+ }
3037
+ if (height != null) {
3038
+ _options.setHeight(height);
3039
+ }
3040
+ if (x != null) {
3041
+ _options.setX(x);
3042
+ }
3043
+ if (y != null) {
3044
+ _options.setY(y);
3045
+ }
3046
+
3047
+ // Apply new dimensions
3048
+ applyDimensions();
3049
+ }
3050
+
3051
+ /**
3052
+ * Convert density-independent pixels (dp) to actual pixels
3053
+ */
3054
+ private float getPixels(int dp) {
3055
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, _context.getResources().getDisplayMetrics());
3056
+ }
262
3057
  }