@capgo/inappbrowser 8.0.0 → 8.0.1

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