@capgo/inappbrowser 7.0.0 → 7.1.6

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