@capgo/inappbrowser 7.0.0 → 7.1.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.
@@ -1,46 +1,167 @@
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;
5
12
  import android.graphics.Bitmap;
13
+ import android.graphics.Color;
14
+ import android.graphics.Picture;
15
+ import android.graphics.drawable.PictureDrawable;
16
+ import android.net.Uri;
17
+ import android.net.http.SslError;
18
+ import android.os.Build;
6
19
  import android.text.TextUtils;
20
+ import android.util.Base64;
21
+ import android.util.Log;
7
22
  import android.view.View;
8
23
  import android.view.Window;
9
24
  import android.view.WindowManager;
25
+ import android.webkit.HttpAuthHandler;
26
+ import android.webkit.JavascriptInterface;
27
+ import android.webkit.PermissionRequest;
28
+ import android.webkit.SslErrorHandler;
29
+ import android.webkit.ValueCallback;
30
+ import android.webkit.WebChromeClient;
10
31
  import android.webkit.WebResourceError;
11
32
  import android.webkit.WebResourceRequest;
33
+ import android.webkit.WebResourceResponse;
12
34
  import android.webkit.WebView;
13
35
  import android.webkit.WebViewClient;
14
36
  import android.widget.ImageButton;
15
37
  import android.widget.TextView;
38
+ import android.widget.Toast;
16
39
  import android.widget.Toolbar;
40
+ import androidx.annotation.RequiresApi;
41
+ import com.caverock.androidsvg.SVG;
42
+ import com.caverock.androidsvg.SVGParseException;
43
+ import com.getcapacitor.JSObject;
44
+ import java.io.ByteArrayInputStream;
45
+ import java.io.IOException;
46
+ import java.io.InputStream;
47
+ import java.net.CookiePolicy;
17
48
  import java.net.URI;
18
49
  import java.net.URISyntaxException;
50
+ import java.net.URL;
51
+ import java.nio.charset.StandardCharsets;
52
+ import java.util.Arrays;
19
53
  import java.util.HashMap;
20
54
  import java.util.Iterator;
55
+ import java.util.List;
21
56
  import java.util.Map;
57
+ import java.util.Objects;
58
+ import java.util.UUID;
59
+ import java.util.concurrent.ExecutorService;
60
+ import java.util.concurrent.Executors;
61
+ import java.util.concurrent.Semaphore;
62
+ import java.util.concurrent.TimeUnit;
63
+ import java.util.function.Consumer;
64
+ import java.util.regex.Matcher;
65
+ import java.util.regex.Pattern;
66
+ import org.json.JSONException;
67
+ import org.json.JSONObject;
22
68
 
23
69
  public class WebViewDialog extends Dialog {
24
70
 
71
+ private class ProxiedRequest {
72
+
73
+ private WebResourceResponse response;
74
+ private Semaphore semaphore;
75
+
76
+ public WebResourceResponse getResponse() {
77
+ return response;
78
+ }
79
+
80
+ public ProxiedRequest() {
81
+ this.semaphore = new Semaphore(0);
82
+ this.response = null;
83
+ }
84
+ }
85
+
25
86
  private WebView _webView;
26
87
  private Toolbar _toolbar;
27
88
  private Options _options;
89
+ private Context _context;
90
+ public Activity activity;
28
91
  private boolean isInitialized = false;
92
+ private WebView capacitorWebView;
93
+ private HashMap<String, ProxiedRequest> proxiedRequestsHashmap;
94
+
95
+ Semaphore preShowSemaphore = null;
96
+ String preshowError = null;
97
+
98
+ public PermissionRequest currentPermissionRequest;
99
+ public static final int FILE_CHOOSER_REQUEST_CODE = 1000;
100
+ public ValueCallback<Uri> mUploadMessage;
101
+ public ValueCallback<Uri[]> mFilePathCallback;
102
+ ExecutorService executorService = Executors.newFixedThreadPool(1);
103
+
104
+ public interface PermissionHandler {
105
+ void handleCameraPermissionRequest(PermissionRequest request);
106
+
107
+ void handleMicrophonePermissionRequest(PermissionRequest request);
108
+ }
29
109
 
30
- public WebViewDialog(Context context, int theme, Options options) {
110
+ private PermissionHandler permissionHandler;
111
+
112
+ public WebViewDialog(
113
+ Context context,
114
+ int theme,
115
+ Options options,
116
+ PermissionHandler permissionHandler,
117
+ WebView capacitorWebView
118
+ ) {
31
119
  super(context, theme);
32
120
  this._options = options;
121
+ this._context = context;
122
+ this.permissionHandler = permissionHandler;
33
123
  this.isInitialized = false;
124
+ this.capacitorWebView = capacitorWebView;
125
+ this.proxiedRequestsHashmap = new HashMap<>();
126
+ }
127
+
128
+ public class JavaScriptInterface {
129
+
130
+ @JavascriptInterface
131
+ public void postMessage(String message) {
132
+ // Handle message from JavaScript
133
+ _options.getCallbacks().javascriptCallback(message);
134
+ }
34
135
  }
35
136
 
137
+ public class PreShowScriptInterface {
138
+
139
+ @JavascriptInterface
140
+ public void error(String error) {
141
+ // Handle message from JavaScript
142
+ if (preShowSemaphore != null) {
143
+ preshowError = error;
144
+ preShowSemaphore.release();
145
+ }
146
+ }
147
+
148
+ @JavascriptInterface
149
+ public void success() {
150
+ // Handle message from JavaScript
151
+ if (preShowSemaphore != null) {
152
+ preShowSemaphore.release();
153
+ }
154
+ }
155
+ }
156
+
157
+ @SuppressLint("SetJavaScriptEnabled")
36
158
  public void presentWebView() {
37
159
  requestWindowFeature(Window.FEATURE_NO_TITLE);
38
160
  setCancelable(true);
39
- getWindow()
40
- .setFlags(
41
- WindowManager.LayoutParams.FLAG_FULLSCREEN,
42
- WindowManager.LayoutParams.FLAG_FULLSCREEN
43
- );
161
+ Objects.requireNonNull(getWindow()).setFlags(
162
+ WindowManager.LayoutParams.FLAG_FULLSCREEN,
163
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
164
+ );
44
165
  setContentView(R.layout.activity_browser);
45
166
  getWindow()
46
167
  .setLayout(
@@ -49,23 +170,104 @@ public class WebViewDialog extends Dialog {
49
170
  );
50
171
 
51
172
  this._webView = findViewById(R.id.browser_view);
52
-
173
+ _webView.addJavascriptInterface(
174
+ new JavaScriptInterface(),
175
+ "AndroidInterface"
176
+ );
177
+ _webView.addJavascriptInterface(
178
+ new PreShowScriptInterface(),
179
+ "PreShowScriptInterface"
180
+ );
53
181
  _webView.getSettings().setJavaScriptEnabled(true);
54
182
  _webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
55
183
  _webView.getSettings().setDatabaseEnabled(true);
56
184
  _webView.getSettings().setDomStorageEnabled(true);
185
+ _webView.getSettings().setAllowFileAccess(true);
57
186
  _webView
58
187
  .getSettings()
59
188
  .setPluginState(android.webkit.WebSettings.PluginState.ON);
60
189
  _webView.getSettings().setLoadWithOverviewMode(true);
61
190
  _webView.getSettings().setUseWideViewPort(true);
191
+ _webView.getSettings().setAllowFileAccessFromFileURLs(true);
192
+ _webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
193
+ _webView.getSettings().setMediaPlaybackRequiresUserGesture(false);
194
+
195
+ _webView.setWebViewClient(new WebViewClient());
196
+
197
+ _webView.setWebChromeClient(
198
+ new WebChromeClient() {
199
+ // Enable file open dialog
200
+ @Override
201
+ public boolean onShowFileChooser(
202
+ WebView webView,
203
+ ValueCallback<Uri[]> filePathCallback,
204
+ WebChromeClient.FileChooserParams fileChooserParams
205
+ ) {
206
+ openFileChooser(
207
+ filePathCallback,
208
+ fileChooserParams.getAcceptTypes()[0],
209
+ fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE
210
+ );
211
+ return true;
212
+ }
213
+
214
+ // Grant permissions for cam
215
+ @Override
216
+ public void onPermissionRequest(final PermissionRequest request) {
217
+ Log.i(
218
+ "INAPPBROWSER",
219
+ "onPermissionRequest " + Arrays.toString(request.getResources())
220
+ );
221
+ final String[] requestedResources = request.getResources();
222
+ for (String r : requestedResources) {
223
+ Log.i("INAPPBROWSER", "requestedResources " + r);
224
+ if (r.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
225
+ Log.i("INAPPBROWSER", "RESOURCE_VIDEO_CAPTURE req");
226
+ // Store the permission request
227
+ currentPermissionRequest = request;
228
+ // Initiate the permission request through the plugin
229
+ if (permissionHandler != null) {
230
+ permissionHandler.handleCameraPermissionRequest(request);
231
+ }
232
+ return; // Return here to avoid denying the request
233
+ } else if (r.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
234
+ Log.i("INAPPBROWSER", "RESOURCE_AUDIO_CAPTURE req");
235
+ // Store the permission request
236
+ currentPermissionRequest = request;
237
+ // Initiate the permission request through the plugin
238
+ if (permissionHandler != null) {
239
+ permissionHandler.handleMicrophonePermissionRequest(request);
240
+ }
241
+ return; // Return here to avoid denying the request
242
+ }
243
+ }
244
+ // If no matching permission is found, deny the request
245
+ request.deny();
246
+ }
247
+
248
+ @Override
249
+ public void onPermissionRequestCanceled(PermissionRequest request) {
250
+ super.onPermissionRequestCanceled(request);
251
+ Toast.makeText(
252
+ WebViewDialog.this.activity,
253
+ "Permission Denied",
254
+ Toast.LENGTH_SHORT
255
+ ).show();
256
+ // Handle the denied permission
257
+ if (currentPermissionRequest != null) {
258
+ currentPermissionRequest.deny();
259
+ currentPermissionRequest = null;
260
+ }
261
+ }
262
+ }
263
+ );
62
264
 
63
265
  Map<String, String> requestHeaders = new HashMap<>();
64
266
  if (_options.getHeaders() != null) {
65
267
  Iterator<String> keys = _options.getHeaders().keys();
66
268
  while (keys.hasNext()) {
67
269
  String key = keys.next();
68
- if (TextUtils.equals(key, "User-Agent")) {
270
+ if (TextUtils.equals(key.toLowerCase(), "user-agent")) {
69
271
  _webView
70
272
  .getSettings()
71
273
  .setUserAgentString(_options.getHeaders().getString(key));
@@ -88,13 +290,134 @@ public class WebViewDialog extends Dialog {
88
290
  }
89
291
  }
90
292
 
293
+ public void postMessageToJS(Object detail) {
294
+ if (_webView != null) {
295
+ try {
296
+ JSONObject jsonObject = new JSONObject();
297
+ jsonObject.put("detail", detail);
298
+ String jsonDetail = jsonObject.toString();
299
+ String script =
300
+ "window.dispatchEvent(new CustomEvent('messageFromNative', " +
301
+ jsonDetail +
302
+ "));";
303
+ _webView.post(() -> _webView.evaluateJavascript(script, null));
304
+ } catch (Exception e) {
305
+ Log.e(
306
+ "postMessageToJS",
307
+ "Error sending message to JS: " + e.getMessage()
308
+ );
309
+ }
310
+ }
311
+ }
312
+
313
+ private void injectJavaScriptInterface() {
314
+ String script =
315
+ "if (!window.mobileApp) { " +
316
+ " window.mobileApp = { " +
317
+ " postMessage: function(message) { " +
318
+ " if (window.AndroidInterface) { " +
319
+ " window.AndroidInterface.postMessage(JSON.stringify(message)); " +
320
+ " } " +
321
+ " } " +
322
+ " }; " +
323
+ "}";
324
+ _webView.evaluateJavascript(script, null);
325
+ }
326
+
327
+ private void injectPreShowScript() {
328
+ // String script =
329
+ // "import('https://unpkg.com/darkreader@4.9.89/darkreader.js').then(() => {DarkReader.enable({ brightness: 100, contrast: 90, sepia: 10 });window.PreLoadScriptInterface.finished()})";
330
+
331
+ if (preShowSemaphore != null) {
332
+ return;
333
+ }
334
+
335
+ String script =
336
+ "async function preShowFunction() {\n" +
337
+ _options.getPreShowScript() +
338
+ '\n' +
339
+ "};\n" +
340
+ "preShowFunction().then(() => window.PreShowScriptInterface.success()).catch(err => { console.error('Preshow error', err); window.PreShowScriptInterface.error(JSON.stringify(err, Object.getOwnPropertyNames(err))) })";
341
+
342
+ Log.i(
343
+ "InjectPreShowScript",
344
+ String.format("PreShowScript script:\n%s", script)
345
+ );
346
+
347
+ preShowSemaphore = new Semaphore(0);
348
+ activity.runOnUiThread(
349
+ new Runnable() {
350
+ @Override
351
+ public void run() {
352
+ _webView.evaluateJavascript(script, null);
353
+ }
354
+ }
355
+ );
356
+
357
+ try {
358
+ if (!preShowSemaphore.tryAcquire(10, TimeUnit.SECONDS)) {
359
+ Log.e(
360
+ "InjectPreShowScript",
361
+ "PreShowScript running for over 10 seconds. The plugin will not wait any longer!"
362
+ );
363
+ return;
364
+ }
365
+ if (preshowError != null && !preshowError.isEmpty()) {
366
+ Log.e(
367
+ "InjectPreShowScript",
368
+ "Error within the user-provided preShowFunction: " + preshowError
369
+ );
370
+ }
371
+ } catch (InterruptedException e) {
372
+ Log.e(
373
+ "InjectPreShowScript",
374
+ "Error when calling InjectPreShowScript: " + e.getMessage()
375
+ );
376
+ } finally {
377
+ preShowSemaphore = null;
378
+ preshowError = null;
379
+ }
380
+ }
381
+
382
+ private void openFileChooser(
383
+ ValueCallback<Uri[]> filePathCallback,
384
+ String acceptType,
385
+ boolean isMultiple
386
+ ) {
387
+ mFilePathCallback = filePathCallback;
388
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
389
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
390
+ intent.setType(acceptType); // Default to */*
391
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultiple);
392
+ activity.startActivityForResult(
393
+ Intent.createChooser(intent, "Select File"),
394
+ FILE_CHOOSER_REQUEST_CODE
395
+ );
396
+ }
397
+
398
+ public void reload() {
399
+ _webView.reload();
400
+ }
401
+
402
+ public void destroy() {
403
+ _webView.destroy();
404
+ }
405
+
406
+ public String getUrl() {
407
+ return _webView.getUrl();
408
+ }
409
+
410
+ public void executeScript(String script) {
411
+ _webView.evaluateJavascript(script, null);
412
+ }
413
+
91
414
  public void setUrl(String url) {
92
415
  Map<String, String> requestHeaders = new HashMap<>();
93
416
  if (_options.getHeaders() != null) {
94
417
  Iterator<String> keys = _options.getHeaders().keys();
95
418
  while (keys.hasNext()) {
96
419
  String key = keys.next();
97
- if (TextUtils.equals(key, "User-Agent")) {
420
+ if (TextUtils.equals(key.toLowerCase(), "user-agent")) {
98
421
  _webView
99
422
  .getSettings()
100
423
  .setUserAgentString(_options.getHeaders().getString(key));
@@ -108,11 +431,27 @@ public class WebViewDialog extends Dialog {
108
431
 
109
432
  private void setTitle(String newTitleText) {
110
433
  TextView textView = (TextView) _toolbar.findViewById(R.id.titleText);
111
- textView.setText(newTitleText);
434
+ if (_options.getVisibleTitle()) {
435
+ textView.setText(newTitleText);
436
+ } else {
437
+ textView.setText("");
438
+ }
112
439
  }
113
440
 
114
441
  private void setupToolbar() {
115
442
  _toolbar = this.findViewById(R.id.tool_bar);
443
+ int color = Color.parseColor("#ffffff");
444
+ try {
445
+ color = Color.parseColor(_options.getToolbarColor());
446
+ } catch (IllegalArgumentException e) {
447
+ // Do nothing
448
+ }
449
+ _toolbar.setBackgroundColor(color);
450
+ _toolbar.findViewById(R.id.backButton).setBackgroundColor(color);
451
+ _toolbar.findViewById(R.id.forwardButton).setBackgroundColor(color);
452
+ _toolbar.findViewById(R.id.closeButton).setBackgroundColor(color);
453
+ _toolbar.findViewById(R.id.reloadButton).setBackgroundColor(color);
454
+
116
455
  if (!TextUtils.isEmpty(_options.getTitle())) {
117
456
  this.setTitle(_options.getTitle());
118
457
  } else {
@@ -153,26 +492,200 @@ public class WebViewDialog extends Dialog {
153
492
  new View.OnClickListener() {
154
493
  @Override
155
494
  public void onClick(View view) {
156
- dismiss();
157
- _options.getCallbacks().closeEvent(_webView.getUrl());
495
+ // if closeModal true then display a native modal to check if the user is sure to close the browser
496
+ if (_options.getCloseModal()) {
497
+ new AlertDialog.Builder(_context)
498
+ .setTitle(_options.getCloseModalTitle())
499
+ .setMessage(_options.getCloseModalDescription())
500
+ .setPositiveButton(
501
+ _options.getCloseModalOk(),
502
+ new DialogInterface.OnClickListener() {
503
+ public void onClick(DialogInterface dialog, int which) {
504
+ // Close button clicked, do something
505
+ dismiss();
506
+ _options.getCallbacks().closeEvent(_webView.getUrl());
507
+ _webView.destroy();
508
+ }
509
+ }
510
+ )
511
+ .setNegativeButton(_options.getCloseModalCancel(), null)
512
+ .show();
513
+ } else {
514
+ dismiss();
515
+ _options.getCallbacks().closeEvent(_webView.getUrl());
516
+ _webView.destroy();
517
+ }
158
518
  }
159
519
  }
160
520
  );
161
521
 
522
+ if (_options.showArrow()) {
523
+ closeButton.setBackgroundResource(R.drawable.arrow_forward_enabled);
524
+ }
525
+
526
+ if (_options.getShowReloadButton()) {
527
+ View reloadButton = _toolbar.findViewById(R.id.reloadButton);
528
+ reloadButton.setVisibility(View.VISIBLE);
529
+ reloadButton.setOnClickListener(
530
+ new View.OnClickListener() {
531
+ @Override
532
+ public void onClick(View view) {
533
+ _webView.reload();
534
+ }
535
+ }
536
+ );
537
+ }
538
+
162
539
  if (TextUtils.equals(_options.getToolbarType(), "activity")) {
163
540
  _toolbar.findViewById(R.id.forwardButton).setVisibility(View.GONE);
164
541
  _toolbar.findViewById(R.id.backButton).setVisibility(View.GONE);
542
+ ImageButton buttonNearDoneView = _toolbar.findViewById(
543
+ R.id.buttonNearDone
544
+ );
545
+ buttonNearDoneView.setVisibility(View.GONE);
165
546
  //TODO: Add share button functionality
166
547
  } else if (TextUtils.equals(_options.getToolbarType(), "navigation")) {
548
+ ImageButton buttonNearDoneView = _toolbar.findViewById(
549
+ R.id.buttonNearDone
550
+ );
551
+ buttonNearDoneView.setVisibility(View.GONE);
167
552
  //TODO: Remove share button when implemented
168
553
  } else if (TextUtils.equals(_options.getToolbarType(), "blank")) {
169
554
  _toolbar.setVisibility(View.GONE);
170
555
  } else {
171
556
  _toolbar.findViewById(R.id.forwardButton).setVisibility(View.GONE);
172
557
  _toolbar.findViewById(R.id.backButton).setVisibility(View.GONE);
558
+
559
+ Options.ButtonNearDone buttonNearDone = _options.getButtonNearDone();
560
+ if (buttonNearDone != null) {
561
+ AssetManager assetManager = _context.getAssets();
562
+
563
+ // Open the SVG file from assets
564
+ InputStream inputStream = null;
565
+ try {
566
+ ImageButton buttonNearDoneView = _toolbar.findViewById(
567
+ R.id.buttonNearDone
568
+ );
569
+ buttonNearDoneView.setVisibility(View.VISIBLE);
570
+
571
+ inputStream = assetManager.open(buttonNearDone.getIcon());
572
+
573
+ SVG svg = SVG.getFromInputStream(inputStream);
574
+ Picture picture = svg.renderToPicture(
575
+ buttonNearDone.getWidth(),
576
+ buttonNearDone.getHeight()
577
+ );
578
+ PictureDrawable pictureDrawable = new PictureDrawable(picture);
579
+
580
+ buttonNearDoneView.setImageDrawable(pictureDrawable);
581
+ buttonNearDoneView.setOnClickListener(view ->
582
+ _options.getCallbacks().buttonNearDoneClicked()
583
+ );
584
+ } catch (IOException | SVGParseException e) {
585
+ throw new RuntimeException(e);
586
+ } finally {
587
+ if (inputStream != null) {
588
+ try {
589
+ inputStream.close();
590
+ } catch (IOException e) {
591
+ throw new RuntimeException(e);
592
+ }
593
+ }
594
+ }
595
+ } else {
596
+ ImageButton buttonNearDoneView = _toolbar.findViewById(
597
+ R.id.buttonNearDone
598
+ );
599
+ buttonNearDoneView.setVisibility(View.GONE);
600
+ }
173
601
  }
174
602
  }
175
603
 
604
+ public void handleProxyResultError(String result, String id) {
605
+ Log.i(
606
+ "InAppBrowserProxy",
607
+ String.format(
608
+ "handleProxyResultError: %s, ok: %s id: %s",
609
+ result,
610
+ false,
611
+ id
612
+ )
613
+ );
614
+ ProxiedRequest proxiedRequest = proxiedRequestsHashmap.get(id);
615
+ if (proxiedRequest == null) {
616
+ Log.e("InAppBrowserProxy", "proxiedRequest is null");
617
+ return;
618
+ }
619
+ proxiedRequestsHashmap.remove(id);
620
+ proxiedRequest.semaphore.release();
621
+ }
622
+
623
+ public void handleProxyResultOk(JSONObject result, String id) {
624
+ Log.i(
625
+ "InAppBrowserProxy",
626
+ String.format("handleProxyResultOk: %s, ok: %s, id: %s", result, true, id)
627
+ );
628
+ ProxiedRequest proxiedRequest = proxiedRequestsHashmap.get(id);
629
+ if (proxiedRequest == null) {
630
+ Log.e("InAppBrowserProxy", "proxiedRequest is null");
631
+ return;
632
+ }
633
+ proxiedRequestsHashmap.remove(id);
634
+
635
+ if (result == null) {
636
+ proxiedRequest.semaphore.release();
637
+ return;
638
+ }
639
+
640
+ Map<String, String> responseHeaders = new HashMap<>();
641
+ String body;
642
+ int code;
643
+
644
+ try {
645
+ body = result.getString("body");
646
+ code = result.getInt("code");
647
+ JSONObject headers = result.getJSONObject("headers");
648
+ for (Iterator<String> it = headers.keys(); it.hasNext();) {
649
+ String headerName = it.next();
650
+ String header = headers.getString(headerName);
651
+ responseHeaders.put(headerName, header);
652
+ }
653
+ } catch (JSONException e) {
654
+ Log.e("InAppBrowserProxy", "Cannot parse OK result", e);
655
+ return;
656
+ }
657
+
658
+ String contentType = responseHeaders.get("Content-Type");
659
+ if (contentType == null) {
660
+ contentType = responseHeaders.get("content-type");
661
+ }
662
+ if (contentType == null) {
663
+ Log.e("InAppBrowserProxy", "'Content-Type' header is required");
664
+ return;
665
+ }
666
+
667
+ if (!((100 <= code && code <= 299) || (400 <= code && code <= 599))) {
668
+ Log.e(
669
+ "InAppBrowserProxy",
670
+ String.format("Status code %s outside of the allowed range", code)
671
+ );
672
+ return;
673
+ }
674
+
675
+ WebResourceResponse webResourceResponse = new WebResourceResponse(
676
+ contentType,
677
+ "utf-8",
678
+ new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))
679
+ );
680
+
681
+ webResourceResponse.setStatusCodeAndReasonPhrase(
682
+ code,
683
+ getReasonPhrase(code)
684
+ );
685
+ proxiedRequest.response = webResourceResponse;
686
+ proxiedRequest.semaphore.release();
687
+ }
688
+
176
689
  private void setWebViewClient() {
177
690
  _webView.setWebViewClient(
178
691
  new WebViewClient() {
@@ -181,9 +694,199 @@ public class WebViewDialog extends Dialog {
181
694
  WebView view,
182
695
  WebResourceRequest request
183
696
  ) {
697
+ // HashMap<String, String> map = new HashMap<>();
698
+ // map.put("x-requested-with", null);
699
+ // view.loadUrl(request.getUrl().toString(), map);
700
+ Context context = view.getContext();
701
+ String url = request.getUrl().toString();
702
+
703
+ if (!url.startsWith("https://") && !url.startsWith("http://")) {
704
+ try {
705
+ Intent intent;
706
+ if (url.startsWith("intent://")) {
707
+ intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
708
+ } else {
709
+ intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
710
+ }
711
+
712
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
713
+ context.startActivity(intent);
714
+ return true;
715
+ } catch (ActivityNotFoundException e) {
716
+ // Do nothing
717
+ } catch (URISyntaxException e) {
718
+ // Do nothing
719
+ }
720
+ }
184
721
  return false;
185
722
  }
186
723
 
724
+ private String randomRequestId() {
725
+ return UUID.randomUUID().toString();
726
+ }
727
+
728
+ private String toBase64(String raw) {
729
+ String s = Base64.encodeToString(raw.getBytes(), Base64.NO_WRAP);
730
+ if (s.endsWith("=")) {
731
+ s = s.substring(0, s.length() - 2);
732
+ }
733
+ return s;
734
+ }
735
+
736
+ //
737
+ // void handleRedirect(String currentUrl, Response response) {
738
+ // String loc = response.header("Location");
739
+ // _webView.evaluateJavascript("");
740
+ // }
741
+ //
742
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
743
+ @Override
744
+ public WebResourceResponse shouldInterceptRequest(
745
+ WebView view,
746
+ WebResourceRequest request
747
+ ) {
748
+ Pattern pattern = _options.getProxyRequestsPattern();
749
+ if (pattern == null) {
750
+ return null;
751
+ }
752
+ Matcher matcher = pattern.matcher(request.getUrl().toString());
753
+ if (!matcher.find()) {
754
+ return null;
755
+ }
756
+
757
+ // Requests matches the regex
758
+ if (Objects.equals(request.getMethod(), "POST")) {
759
+ // Log.e("HTTP", String.format("returned null (ok) %s", request.getUrl().toString()));
760
+ return null;
761
+ }
762
+
763
+ Log.i(
764
+ "InAppBrowserProxy",
765
+ String.format("Proxying request: %s", request.getUrl().toString())
766
+ );
767
+
768
+ // We need to call a JS function
769
+ String requestId = randomRequestId();
770
+ ProxiedRequest proxiedRequest = new ProxiedRequest();
771
+ proxiedRequestsHashmap.put(requestId, proxiedRequest);
772
+
773
+ // lsuakdchgbbaHandleProxiedRequest
774
+ activity.runOnUiThread(
775
+ new Runnable() {
776
+ @Override
777
+ public void run() {
778
+ StringBuilder headers = new StringBuilder();
779
+ Map<String, String> requestHeaders =
780
+ request.getRequestHeaders();
781
+ for (Map.Entry<
782
+ String,
783
+ String
784
+ > header : requestHeaders.entrySet()) {
785
+ headers.append(
786
+ String.format(
787
+ "h[atob('%s')]=atob('%s');",
788
+ toBase64(header.getKey()),
789
+ toBase64(header.getValue())
790
+ )
791
+ );
792
+ }
793
+ String s = String.format(
794
+ "try {function getHeaders() {const h = {}; %s return h}; window.InAppBrowserProxyRequest(new Request(atob('%s'), {headers: getHeaders(), method: '%s'})).then(async (res) => Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({ok: true, result: (!!res ? {headers: Object.fromEntries(res.headers.entries()), code: res.status, body: (await res.text())} : null), id: '%s'})).catch((e) => Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({ok: false, result: e.toString(), id: '%s'}))} catch (e) {Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({ok: false, result: e.toString(), id: '%s'})}",
795
+ headers,
796
+ toBase64(request.getUrl().toString()),
797
+ request.getMethod(),
798
+ requestId,
799
+ requestId,
800
+ requestId
801
+ );
802
+ // Log.i("HTTP", s);
803
+ capacitorWebView.evaluateJavascript(s, null);
804
+ }
805
+ }
806
+ );
807
+
808
+ // 10 seconds wait max
809
+ try {
810
+ if (proxiedRequest.semaphore.tryAcquire(1, 10, TimeUnit.SECONDS)) {
811
+ return proxiedRequest.response;
812
+ } else {
813
+ Log.e("InAppBrowserProxy", "Semaphore timed out");
814
+ proxiedRequestsHashmap.remove(requestId); // prevent mem leak
815
+ }
816
+ } catch (InterruptedException e) {
817
+ Log.e("InAppBrowserProxy", "Semaphore wait error", e);
818
+ }
819
+ return null;
820
+ }
821
+
822
+ @Override
823
+ public void onReceivedHttpAuthRequest(
824
+ WebView view,
825
+ HttpAuthHandler handler,
826
+ String host,
827
+ String realm
828
+ ) {
829
+ final String sourceUrl = _options.getUrl();
830
+ final String url = view.getUrl();
831
+ final JSObject credentials = _options.getCredentials();
832
+
833
+ if (
834
+ credentials != null &&
835
+ credentials.getString("username") != null &&
836
+ credentials.getString("password") != null &&
837
+ sourceUrl != null &&
838
+ url != null
839
+ ) {
840
+ String sourceProtocol = "";
841
+ String sourceHost = "";
842
+ int sourcePort = -1;
843
+ try {
844
+ URI uri = new URI(sourceUrl);
845
+ sourceProtocol = uri.getScheme();
846
+ sourceHost = uri.getHost();
847
+ sourcePort = uri.getPort();
848
+ if (
849
+ sourcePort == -1 && Objects.equals(sourceProtocol, "https")
850
+ ) sourcePort = 443;
851
+ else if (
852
+ sourcePort == -1 && Objects.equals(sourceProtocol, "http")
853
+ ) sourcePort = 80;
854
+ } catch (URISyntaxException e) {
855
+ e.printStackTrace();
856
+ }
857
+
858
+ String protocol = "";
859
+ int port = -1;
860
+ try {
861
+ URI uri = new URI(url);
862
+ protocol = uri.getScheme();
863
+ port = uri.getPort();
864
+ if (port == -1 && Objects.equals(protocol, "https")) port = 443;
865
+ else if (port == -1 && Objects.equals(protocol, "http")) port =
866
+ 80;
867
+ } catch (URISyntaxException e) {
868
+ e.printStackTrace();
869
+ }
870
+
871
+ if (
872
+ Objects.equals(sourceHost, host) &&
873
+ Objects.equals(sourceProtocol, protocol) &&
874
+ sourcePort == port
875
+ ) {
876
+ final String username = Objects.requireNonNull(
877
+ credentials.getString("username")
878
+ );
879
+ final String password = Objects.requireNonNull(
880
+ credentials.getString("password")
881
+ );
882
+ handler.proceed(username, password);
883
+ return;
884
+ }
885
+ }
886
+
887
+ super.onReceivedHttpAuthRequest(view, handler, host, realm);
888
+ }
889
+
187
890
  @Override
188
891
  public void onLoadResource(WebView view, String url) {
189
892
  super.onLoadResource(view, url);
@@ -194,24 +897,77 @@ public class WebViewDialog extends Dialog {
194
897
  super.onPageStarted(view, url, favicon);
195
898
  try {
196
899
  URI uri = new URI(url);
197
- setTitle(uri.getHost());
900
+ if (TextUtils.isEmpty(_options.getTitle())) {
901
+ setTitle(uri.getHost());
902
+ }
198
903
  } catch (URISyntaxException e) {
199
904
  // Do nothing
200
905
  }
201
- _options.getCallbacks().urlChangeEvent(url);
906
+ }
907
+
908
+ public void doUpdateVisitedHistory(
909
+ WebView view,
910
+ String url,
911
+ boolean isReload
912
+ ) {
913
+ if (!isReload) {
914
+ _options.getCallbacks().urlChangeEvent(url);
915
+ }
916
+ super.doUpdateVisitedHistory(view, url, isReload);
917
+ injectJavaScriptInterface();
202
918
  }
203
919
 
204
920
  @Override
205
921
  public void onPageFinished(WebView view, String url) {
206
922
  super.onPageFinished(view, url);
207
- _options.getCallbacks().pageLoaded();
208
923
  if (!isInitialized) {
209
924
  isInitialized = true;
210
925
  _webView.clearHistory();
211
926
  if (_options.isPresentAfterPageLoad()) {
212
- show();
213
- _options.getPluginCall().resolve();
927
+ boolean usePreShowScript =
928
+ _options.getPreShowScript() != null &&
929
+ !_options.getPreShowScript().isEmpty();
930
+ if (!usePreShowScript) {
931
+ show();
932
+ _options.getPluginCall().resolve();
933
+ } else {
934
+ executorService.execute(
935
+ new Runnable() {
936
+ @Override
937
+ public void run() {
938
+ if (
939
+ _options.getPreShowScript() != null &&
940
+ !_options.getPreShowScript().isEmpty()
941
+ ) {
942
+ injectPreShowScript();
943
+ }
944
+
945
+ activity.runOnUiThread(
946
+ new Runnable() {
947
+ @Override
948
+ public void run() {
949
+ show();
950
+ _options.getPluginCall().resolve();
951
+ }
952
+ }
953
+ );
954
+ }
955
+ }
956
+ );
957
+ }
214
958
  }
959
+ } else if (
960
+ _options.getPreShowScript() != null &&
961
+ !_options.getPreShowScript().isEmpty()
962
+ ) {
963
+ executorService.execute(
964
+ new Runnable() {
965
+ @Override
966
+ public void run() {
967
+ injectPreShowScript();
968
+ }
969
+ }
970
+ );
215
971
  }
216
972
 
217
973
  ImageButton backButton = _toolbar.findViewById(R.id.backButton);
@@ -233,6 +989,7 @@ public class WebViewDialog extends Dialog {
233
989
  }
234
990
 
235
991
  _options.getCallbacks().pageLoaded();
992
+ injectJavaScriptInterface();
236
993
  }
237
994
 
238
995
  @Override
@@ -244,6 +1001,23 @@ public class WebViewDialog extends Dialog {
244
1001
  super.onReceivedError(view, request, error);
245
1002
  _options.getCallbacks().pageLoadError();
246
1003
  }
1004
+
1005
+ @SuppressLint("WebViewClientOnReceivedSslError")
1006
+ @Override
1007
+ public void onReceivedSslError(
1008
+ WebView view,
1009
+ SslErrorHandler handler,
1010
+ SslError error
1011
+ ) {
1012
+ boolean ignoreSSLUntrustedError = _options.ignoreUntrustedSSLError();
1013
+ if (
1014
+ ignoreSSLUntrustedError &&
1015
+ error.getPrimaryError() == SslError.SSL_UNTRUSTED
1016
+ ) handler.proceed();
1017
+ else {
1018
+ super.onReceivedSslError(view, handler, error);
1019
+ }
1020
+ }
247
1021
  }
248
1022
  );
249
1023
  }
@@ -252,11 +1026,109 @@ public class WebViewDialog extends Dialog {
252
1026
  public void onBackPressed() {
253
1027
  if (
254
1028
  _webView.canGoBack() &&
255
- TextUtils.equals(_options.getToolbarType(), "navigation")
1029
+ (TextUtils.equals(_options.getToolbarType(), "navigation") ||
1030
+ _options.getActiveNativeNavigationForWebview())
256
1031
  ) {
257
1032
  _webView.goBack();
258
- } else {
1033
+ } else if (!_options.getDisableGoBackOnNativeApplication()) {
259
1034
  super.onBackPressed();
260
1035
  }
261
1036
  }
1037
+
1038
+ public static String getReasonPhrase(int statusCode) {
1039
+ switch (statusCode) {
1040
+ case (200):
1041
+ return "OK";
1042
+ case (201):
1043
+ return "Created";
1044
+ case (202):
1045
+ return "Accepted";
1046
+ case (203):
1047
+ return "Non Authoritative Information";
1048
+ case (204):
1049
+ return "No Content";
1050
+ case (205):
1051
+ return "Reset Content";
1052
+ case (206):
1053
+ return "Partial Content";
1054
+ case (207):
1055
+ return "Partial Update OK";
1056
+ case (300):
1057
+ return "Mutliple Choices";
1058
+ case (301):
1059
+ return "Moved Permanently";
1060
+ case (302):
1061
+ return "Moved Temporarily";
1062
+ case (303):
1063
+ return "See Other";
1064
+ case (304):
1065
+ return "Not Modified";
1066
+ case (305):
1067
+ return "Use Proxy";
1068
+ case (307):
1069
+ return "Temporary Redirect";
1070
+ case (400):
1071
+ return "Bad Request";
1072
+ case (401):
1073
+ return "Unauthorized";
1074
+ case (402):
1075
+ return "Payment Required";
1076
+ case (403):
1077
+ return "Forbidden";
1078
+ case (404):
1079
+ return "Not Found";
1080
+ case (405):
1081
+ return "Method Not Allowed";
1082
+ case (406):
1083
+ return "Not Acceptable";
1084
+ case (407):
1085
+ return "Proxy Authentication Required";
1086
+ case (408):
1087
+ return "Request Timeout";
1088
+ case (409):
1089
+ return "Conflict";
1090
+ case (410):
1091
+ return "Gone";
1092
+ case (411):
1093
+ return "Length Required";
1094
+ case (412):
1095
+ return "Precondition Failed";
1096
+ case (413):
1097
+ return "Request Entity Too Large";
1098
+ case (414):
1099
+ return "Request-URI Too Long";
1100
+ case (415):
1101
+ return "Unsupported Media Type";
1102
+ case (416):
1103
+ return "Requested Range Not Satisfiable";
1104
+ case (417):
1105
+ return "Expectation Failed";
1106
+ case (418):
1107
+ return "Reauthentication Required";
1108
+ case (419):
1109
+ return "Proxy Reauthentication Required";
1110
+ case (422):
1111
+ return "Unprocessable Entity";
1112
+ case (423):
1113
+ return "Locked";
1114
+ case (424):
1115
+ return "Failed Dependency";
1116
+ case (500):
1117
+ return "Server Error";
1118
+ case (501):
1119
+ return "Not Implemented";
1120
+ case (502):
1121
+ return "Bad Gateway";
1122
+ case (503):
1123
+ return "Service Unavailable";
1124
+ case (504):
1125
+ return "Gateway Timeout";
1126
+ case (505):
1127
+ return "HTTP Version Not Supported";
1128
+ case (507):
1129
+ return "Insufficient Storage";
1130
+ default:
1131
+ return "";
1132
+ }
1133
+ }
262
1134
  }