@capgo/capacitor-inappbrowser 8.19.0 → 8.20.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.
@@ -40,6 +40,7 @@ import android.util.Log;
40
40
  import android.util.TypedValue;
41
41
  import android.view.Gravity;
42
42
  import android.view.KeyEvent;
43
+ import android.view.LayoutInflater;
43
44
  import android.view.MotionEvent;
44
45
  import android.view.View;
45
46
  import android.view.ViewGroup;
@@ -74,6 +75,8 @@ import androidx.activity.OnBackPressedCallback;
74
75
  import androidx.activity.result.ActivityResult;
75
76
  import androidx.activity.result.ActivityResultLauncher;
76
77
  import androidx.activity.result.contract.ActivityResultContracts;
78
+ import androidx.appcompat.view.ContextThemeWrapper;
79
+ import androidx.appcompat.widget.TooltipCompat;
77
80
  import androidx.coordinatorlayout.widget.CoordinatorLayout;
78
81
  import androidx.core.content.FileProvider;
79
82
  import androidx.core.content.res.ResourcesCompat;
@@ -91,6 +94,8 @@ import com.caverock.androidsvg.SVG;
91
94
  import com.caverock.androidsvg.SVGParseException;
92
95
  import com.getcapacitor.JSObject;
93
96
  import com.getcapacitor.PluginCall;
97
+ import com.google.android.material.button.MaterialButton;
98
+ import com.google.android.material.color.DynamicColors;
94
99
  import java.io.ByteArrayInputStream;
95
100
  import java.io.ByteArrayOutputStream;
96
101
  import java.io.File;
@@ -338,6 +343,154 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
338
343
  private SwipeRefreshLayout swipeRefreshLayout;
339
344
  private boolean reloadFromGestureInProgress = false;
340
345
  private WebView _webView;
346
+ private FullscreenWindowState nativeFullscreenWindow;
347
+ private FullscreenWindowState mediaFullscreenWindow;
348
+ private MaterialButton fullscreenExitButton;
349
+ private java.util.function.Consumer<Boolean> fullscreenChangeListener;
350
+ private final java.util.Map<View, Integer> fullscreenVisibility = new java.util.HashMap<>();
351
+ private int[] fullscreenPadding;
352
+ private CoordinatorLayout.Behavior<?> fullscreenContentBehavior;
353
+ private String fullscreenOrigin;
354
+
355
+ public void setFullscreenChangeListener(java.util.function.Consumer<Boolean> listener) {
356
+ fullscreenChangeListener = listener;
357
+ }
358
+
359
+ public boolean isFullscreen() {
360
+ return nativeFullscreenWindow != null;
361
+ }
362
+
363
+ public void clearFullscreen() {
364
+ if (_options != null) _options.setFullscreen(false);
365
+ exitCustomFullscreenView();
366
+ setFullscreen(false);
367
+ }
368
+
369
+ public void setFullscreen(boolean enabled) {
370
+ if (!enabled && _options != null) _options.setFullscreen(false);
371
+ if (enabled == isFullscreen()) return;
372
+ if (enabled) {
373
+ if (
374
+ _webView == null ||
375
+ !activeForBackNavigation ||
376
+ isDismissing ||
377
+ isHiddenModeActive ||
378
+ (_options != null &&
379
+ (_options.isHidden() ||
380
+ _options.isToBack() ||
381
+ _options.getWidth() != null ||
382
+ _options.getHeight() != null ||
383
+ _options.getX() != null ||
384
+ _options.getY() != null)) ||
385
+ backLayerActive ||
386
+ getWindow() == null
387
+ ) {
388
+ throw new IllegalStateException("Fullscreen requires a visible, full-size front WebView");
389
+ }
390
+ // Finish media first: its saved window state must not outlive the browser baseline.
391
+ exitCustomFullscreenView();
392
+ nativeFullscreenWindow = new FullscreenWindowState(getWindow());
393
+ fullscreenOrigin = origin(_webView.getUrl() != null ? _webView.getUrl() : _options.getUrl());
394
+ for (int id : new int[] { R.id.app_bar_layout, R.id.status_bar_color_view }) {
395
+ View view = findBrowserContentDescendant(id);
396
+ if (view != null) {
397
+ fullscreenVisibility.put(view, view.getVisibility());
398
+ view.setVisibility(View.GONE);
399
+ }
400
+ }
401
+ View container = findBrowserContentDescendant(R.id.content_browser_layout);
402
+ if (container != null) {
403
+ fullscreenPadding = new int[] {
404
+ container.getPaddingLeft(),
405
+ container.getPaddingTop(),
406
+ container.getPaddingRight(),
407
+ container.getPaddingBottom()
408
+ };
409
+ container.setPadding(0, 0, 0, 0);
410
+ if (container.getLayoutParams() instanceof CoordinatorLayout.LayoutParams params) {
411
+ // A hidden appbar retains its measured height; its scrolling behavior still reserves that space.
412
+ fullscreenContentBehavior = params.getBehavior();
413
+ params.setBehavior(null);
414
+ container.setLayoutParams(params);
415
+ }
416
+ }
417
+ nativeFullscreenWindow.enter();
418
+ // Scope Material styling to the overlay, including hosts without a Material theme.
419
+ Context buttonContext = DynamicColors.wrapContextIfAvailable(
420
+ new ContextThemeWrapper(getContext(), com.google.android.material.R.style.Theme_Material3_DayNight_NoActionBar)
421
+ );
422
+ ViewGroup decor = (ViewGroup) getWindow().getDecorView();
423
+ fullscreenExitButton = (MaterialButton) LayoutInflater.from(buttonContext).inflate(
424
+ R.layout.fullscreen_exit_button,
425
+ decor,
426
+ false
427
+ );
428
+ TooltipCompat.setTooltipText(fullscreenExitButton, getContext().getString(R.string.exit_fullscreen));
429
+ fullscreenExitButton.setOnClickListener((v) -> clearFullscreen());
430
+ decor.addView(fullscreenExitButton);
431
+ ViewCompat.setOnApplyWindowInsetsListener(fullscreenExitButton, (v, insets) -> {
432
+ updateFullscreenExitInsets(insets);
433
+ return insets;
434
+ });
435
+ ViewCompat.requestApplyInsets(fullscreenExitButton);
436
+ } else {
437
+ exitCustomFullscreenView();
438
+ FullscreenWindowState baseline = nativeFullscreenWindow;
439
+ nativeFullscreenWindow = null;
440
+ if (fullscreenExitButton != null && fullscreenExitButton.getParent() instanceof ViewGroup parent) parent.removeView(
441
+ fullscreenExitButton
442
+ );
443
+ fullscreenExitButton = null;
444
+ for (java.util.Map.Entry<View, Integer> entry : fullscreenVisibility.entrySet()) entry.getKey().setVisibility(entry.getValue());
445
+ fullscreenVisibility.clear();
446
+ View container = findBrowserContentDescendant(R.id.content_browser_layout);
447
+ if (container != null && fullscreenPadding != null) container.setPadding(
448
+ fullscreenPadding[0],
449
+ fullscreenPadding[1],
450
+ fullscreenPadding[2],
451
+ fullscreenPadding[3]
452
+ );
453
+ fullscreenPadding = null;
454
+ if (container != null && container.getLayoutParams() instanceof CoordinatorLayout.LayoutParams params) {
455
+ params.setBehavior(fullscreenContentBehavior);
456
+ container.setLayoutParams(params);
457
+ }
458
+ fullscreenContentBehavior = null;
459
+ baseline.restore();
460
+ reapplyInsetsFromWindowRoot();
461
+ }
462
+ if (fullscreenChangeListener != null) fullscreenChangeListener.accept(enabled);
463
+ }
464
+
465
+ private void updateFullscreenExitInsets(WindowInsetsCompat insets) {
466
+ if (fullscreenExitButton == null || insets == null) return;
467
+ // Hidden status bars need no space in landscape; cutouts and navigation gestures always do.
468
+ Insets safe = Insets.max(
469
+ insets.getInsets(WindowInsetsCompat.Type.systemBars()),
470
+ insets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars() | WindowInsetsCompat.Type.displayCutout())
471
+ );
472
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) fullscreenExitButton.getLayoutParams();
473
+ int margin = getContext().getResources().getDimensionPixelSize(R.dimen.fullscreen_exit_margin);
474
+ params.topMargin = safe.top + margin;
475
+ params.rightMargin = safe.right + margin;
476
+ fullscreenExitButton.setLayoutParams(params);
477
+ }
478
+
479
+ private static String origin(String url) {
480
+ if (url == null) return null;
481
+ try {
482
+ URI uri = new URI(url);
483
+ String scheme = uri.getScheme();
484
+ String host = uri.getHost();
485
+ if (scheme == null || host == null) return url;
486
+ int port = uri.getPort();
487
+ if (port == -1) port = "https".equalsIgnoreCase(scheme) ? 443 : 80;
488
+ return scheme.toLowerCase(java.util.Locale.ROOT) + "://" + host.toLowerCase(java.util.Locale.ROOT) + ":" + port;
489
+ } catch (Exception error) {
490
+ return url;
491
+ }
492
+ }
493
+
341
494
  // HTML5/iframe fullscreen (e.g. embedded YouTube) routes through WebChromeClient custom views.
342
495
  private View customFullscreenView;
343
496
  private WebChromeClient.CustomViewCallback customViewCallback;
@@ -428,6 +581,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
428
581
  private ActivityResultLauncher<Intent> fileChooserLauncher;
429
582
  private boolean openWebViewResolved;
430
583
  private boolean isDismissing = false;
584
+ private AlertDialog backCloseConfirmDialog;
431
585
  private PermissionRequest pendingCameraLaunchPermissionRequest;
432
586
 
433
587
  // Temporary URI for storing camera capture
@@ -469,11 +623,24 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
469
623
  @Override
470
624
  protected void onStart() {
471
625
  super.onStart();
626
+ applyStartupFullscreen();
472
627
  // ComponentDialog registers its own back callback in onCreate; re-add ours last so
473
628
  // handleBrowserBackNavigation runs before the built-in cancel callback (LIFO order).
474
629
  ensureDialogBackHandlerOnTop();
475
630
  }
476
631
 
632
+ private void applyStartupFullscreen() {
633
+ if (_options != null && _options.isFullscreen() && !_options.isHidden()) {
634
+ _options.setFullscreen(false);
635
+ try {
636
+ setFullscreen(true);
637
+ } catch (IllegalStateException error) {
638
+ // Deferred presentation or page-initiated show may target an inactive WebView.
639
+ Log.w("InAppBrowser", "Skipping deferred fullscreen entry: " + error.getMessage());
640
+ }
641
+ }
642
+ }
643
+
477
644
  @Override
478
645
  public void show() {
479
646
  super.show();
@@ -698,6 +865,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
698
865
  }
699
866
 
700
867
  public boolean sendToBack(boolean transparentBackground) {
868
+ clearFullscreen();
701
869
  if (_options != null) {
702
870
  _options.setToBack(true);
703
871
  _options.setTransparentBackground(transparentBackground);
@@ -900,6 +1068,10 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
900
1068
 
901
1069
  activity.runOnUiThread(() -> {
902
1070
  try {
1071
+ if (isDismissing) {
1072
+ // Dismissal already started (e.g. close button or back); do not close twice.
1073
+ return;
1074
+ }
903
1075
  String currentUrl = getUrl();
904
1076
  dismiss();
905
1077
 
@@ -925,7 +1097,10 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
925
1097
  Log.e("InAppBrowser", "Cannot hide - activity is null");
926
1098
  return;
927
1099
  }
928
- activity.runOnUiThread(() -> setHidden(true));
1100
+ activity.runOnUiThread(() -> {
1101
+ clearFullscreen();
1102
+ setHidden(true);
1103
+ });
929
1104
  }
930
1105
 
931
1106
  @JavascriptInterface
@@ -2162,7 +2337,10 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
2162
2337
  setContentView(R.layout.activity_browser);
2163
2338
 
2164
2339
  // If custom dimensions are set, configure for touch passthrough
2165
- if (_options != null && (_options.getWidth() != null || _options.getHeight() != null)) {
2340
+ if (
2341
+ _options != null &&
2342
+ (_options.getWidth() != null || _options.getHeight() != null || _options.getX() != null || _options.getY() != null)
2343
+ ) {
2166
2344
  Window window = getWindow();
2167
2345
  if (window != null) {
2168
2346
  // Make the dialog background transparent
@@ -2801,6 +2979,10 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
2801
2979
  public void onCloseWindow(WebView window) {
2802
2980
  Log.d("InAppBrowser", "onCloseWindow called");
2803
2981
  if (window == _webView) {
2982
+ if (isDismissing) {
2983
+ // Dismissal already started (e.g. close button or back); do not close twice.
2984
+ return;
2985
+ }
2804
2986
  String currentUrl = getUrl();
2805
2987
  dismiss();
2806
2988
  if (_options != null && _options.getCallbacks() != null) {
@@ -2850,9 +3032,9 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
2850
3032
  }
2851
3033
 
2852
3034
  try {
2853
- loadInitialWebViewContent(requestHeaders);
2854
3035
  setupToolbar();
2855
3036
  setWebViewClient();
3037
+ loadInitialWebViewContent(requestHeaders);
2856
3038
 
2857
3039
  if (this._options.isHidden()) {
2858
3040
  if (_options.isPopupWindowMode() || _options.getInvisibilityMode() == Options.InvisibilityMode.FAKE_VISIBLE) {
@@ -3002,6 +3184,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3002
3184
  if (activeForBackNavigation == active) {
3003
3185
  return;
3004
3186
  }
3187
+ if (!active && (isFullscreen() || customFullscreenView != null)) clearFullscreen();
3005
3188
  activeForBackNavigation = active;
3006
3189
  syncBackNavigationHandlers();
3007
3190
  }
@@ -3030,10 +3213,18 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3030
3213
  }
3031
3214
 
3032
3215
  private boolean shouldConsumeBackPress() {
3033
- return WebViewBackNavigationSupport.shouldConsumeBackPress(isShowing(), isHiddenModeActive, backLayerActive);
3216
+ return isFullscreen() || WebViewBackNavigationSupport.shouldConsumeBackPress(isShowing(), isHiddenModeActive, backLayerActive);
3034
3217
  }
3035
3218
 
3036
3219
  private void handleBrowserBackNavigation() {
3220
+ if (WebViewCustomFullscreenSupport.shouldConsumeBackPress(customFullscreenView != null)) {
3221
+ exitCustomFullscreenView();
3222
+ return;
3223
+ }
3224
+ if (isFullscreen()) {
3225
+ clearFullscreen();
3226
+ return;
3227
+ }
3037
3228
  if (_options == null) {
3038
3229
  dismiss();
3039
3230
  return;
@@ -3061,6 +3252,26 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3061
3252
  case DISMISS:
3062
3253
  default:
3063
3254
  String currentUrl = getUrl();
3255
+ Pattern urlPattern = _options.getCloseModalURLPattern();
3256
+ if (_options.getCloseModal() && (urlPattern == null || urlPattern.matcher(currentUrl).find())) {
3257
+ // Confirm like the toolbar close button; OK does the same close as below
3258
+ backCloseConfirmDialog = new AlertDialog.Builder(_context)
3259
+ .setTitle(_options.getCloseModalTitle())
3260
+ .setMessage(_options.getCloseModalDescription())
3261
+ .setPositiveButton(_options.getCloseModalOk(), (dialog, which) -> {
3262
+ if (isDismissing) {
3263
+ return;
3264
+ }
3265
+ if (_options.getCallbacks() != null) {
3266
+ _options.getCallbacks().confirmBtnClicked(currentUrl);
3267
+ _options.getCallbacks().closeEvent(currentUrl);
3268
+ }
3269
+ dismiss();
3270
+ })
3271
+ .setNegativeButton(_options.getCloseModalCancel(), null)
3272
+ .show();
3273
+ return;
3274
+ }
3064
3275
  if (_options.getCallbacks() != null) {
3065
3276
  _options.getCallbacks().closeEvent(currentUrl);
3066
3277
  }
@@ -3106,8 +3317,9 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3106
3317
  _webView.setVisibility(View.INVISIBLE);
3107
3318
  }
3108
3319
 
3109
- window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
3110
- WebViewCustomFullscreenSupport.enterImmersiveFullscreen(window, decorView);
3320
+ mediaFullscreenWindow = new FullscreenWindowState(window);
3321
+ mediaFullscreenWindow.enter();
3322
+ if (fullscreenExitButton != null) fullscreenExitButton.bringToFront();
3111
3323
 
3112
3324
  if (WebViewCustomFullscreenSupport.shouldRegisterHostBackHandler(backLayerActive)) {
3113
3325
  registerCustomFullscreenBackHandler();
@@ -3162,15 +3374,12 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3162
3374
  _webView.setVisibility(View.VISIBLE);
3163
3375
  }
3164
3376
 
3165
- Window window = customFullscreenWindow != null ? customFullscreenWindow : getWindow();
3166
- if (window != null) {
3167
- window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
3168
- WebViewCustomFullscreenSupport.exitImmersiveFullscreen(window, window.getDecorView());
3169
- reapplyInsetsFromWindowRoot();
3170
- if (SystemUiChromeSupport.requiresEdgeToEdgeChrome(Build.VERSION.SDK_INT)) {
3171
- refreshEdgeToEdgeChrome();
3172
- }
3377
+ if (mediaFullscreenWindow != null) {
3378
+ mediaFullscreenWindow.restore();
3379
+ mediaFullscreenWindow = null;
3173
3380
  }
3381
+ if (nativeFullscreenWindow != null) nativeFullscreenWindow.enter();
3382
+ else reapplyInsetsFromWindowRoot();
3174
3383
 
3175
3384
  unregisterCustomFullscreenBackHandler();
3176
3385
 
@@ -3275,6 +3484,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3275
3484
  }
3276
3485
 
3277
3486
  public void setHidden(boolean hidden) {
3487
+ if (hidden && (isFullscreen() || (_options != null && !_options.isHidden()))) clearFullscreen();
3278
3488
  if (hidden) {
3279
3489
  if (!isHiddenModeActive) {
3280
3490
  if (getWindow() == null) {
@@ -3314,6 +3524,9 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3314
3524
  }
3315
3525
  if (_options != null) {
3316
3526
  _options.setHidden(hidden);
3527
+ if (isShowing()) {
3528
+ applyStartupFullscreen();
3529
+ }
3317
3530
  }
3318
3531
  }
3319
3532
 
@@ -3508,6 +3721,11 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3508
3721
  * after portrait↔landscape transitions.
3509
3722
  */
3510
3723
  private void refreshLayoutForConfigurationChange() {
3724
+ if (isFullscreen()) {
3725
+ nativeFullscreenWindow.enter();
3726
+ updateFullscreenExitInsets(ViewCompat.getRootWindowInsets(getWindow().getDecorView()));
3727
+ return;
3728
+ }
3511
3729
  if (isDismissing || _webView == null) {
3512
3730
  return;
3513
3731
  }
@@ -3684,6 +3902,18 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3684
3902
  return;
3685
3903
  }
3686
3904
 
3905
+ if (isFullscreen()) {
3906
+ updateFullscreenExitInsets(windowInsets);
3907
+ View container = findBrowserContentDescendant(R.id.content_browser_layout);
3908
+ if (container != null) {
3909
+ // FLAG_FULLSCREEN disables adjustResize even before Android 15; keep only the keyboard inset.
3910
+ int imeBottom = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
3911
+ ? windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
3912
+ : 0;
3913
+ container.setPadding(0, 0, 0, imeBottom);
3914
+ }
3915
+ return;
3916
+ }
3687
3917
  Insets bars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
3688
3918
  Insets navigationBars = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars());
3689
3919
  Insets systemGestures = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures());
@@ -6316,9 +6546,20 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
6316
6546
  super.onLoadResource(view, url);
6317
6547
  }
6318
6548
 
6549
+ @Override
6550
+ public boolean onRenderProcessGone(WebView view, android.webkit.RenderProcessGoneDetail detail) {
6551
+ clearFullscreen();
6552
+ dismiss();
6553
+ return true;
6554
+ }
6555
+
6319
6556
  @Override
6320
6557
  public void onPageStarted(WebView view, String url, Bitmap favicon) {
6321
6558
  super.onPageStarted(view, url, favicon);
6559
+ if (
6560
+ (isFullscreen() && !java.util.Objects.equals(fullscreenOrigin, origin(url))) ||
6561
+ (_options.isFullscreen() && !java.util.Objects.equals(origin(_options.getUrl()), origin(url)))
6562
+ ) clearFullscreen();
6322
6563
  if (view == null || _webView == null) {
6323
6564
  return;
6324
6565
  }
@@ -6361,13 +6602,13 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
6361
6602
  @Override
6362
6603
  public void onPageFinished(WebView view, String url) {
6363
6604
  super.onPageFinished(view, url);
6364
- if (view == null || _webView == null) {
6605
+ if (view == null || _webView != view || isDismissing) {
6365
6606
  return;
6366
6607
  }
6367
6608
  if (!isInitialized) {
6368
6609
  isInitialized = true;
6369
6610
  _webView.clearHistory();
6370
- if (_options.isPresentAfterPageLoad()) {
6611
+ if (_options.isPresentAfterPageLoad() && !_options.isHidden()) {
6371
6612
  boolean usePreShowScript =
6372
6613
  _options.getPreShowScript() != null &&
6373
6614
  !_options.getPreShowScript().isEmpty() &&
@@ -6388,7 +6629,9 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
6388
6629
  new Runnable() {
6389
6630
  @Override
6390
6631
  public void run() {
6391
- showAccordingToLayerModeOrFallback();
6632
+ if (isDismissing || _webView != view) return;
6633
+ // Visibility can change while the pre-show script is running.
6634
+ if (!_options.isHidden()) showAccordingToLayerModeOrFallback();
6392
6635
  resolveOpenWebViewIfNeeded();
6393
6636
  }
6394
6637
  }
@@ -6554,7 +6797,11 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
6554
6797
 
6555
6798
  @Override
6556
6799
  public void dismiss() {
6557
- exitCustomFullscreenView();
6800
+ if (backCloseConfirmDialog != null) {
6801
+ backCloseConfirmDialog.dismiss();
6802
+ backCloseConfirmDialog = null;
6803
+ }
6804
+ clearFullscreen();
6558
6805
  unregisterConfigurationCallbacks();
6559
6806
  scheduleHostWebViewInsetRestore();
6560
6807
  detachBackLayer();
@@ -6962,6 +7209,8 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
6962
7209
  ? Base64.encodeToString(httpBody.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP)
6963
7210
  : "";
6964
7211
 
7212
+ // View.post waits for attachment; deferred presentation waits for this first load to finish.
7213
+ // Dispatch through the main looper so hidden and present-after-load WebViews can start loading.
6965
7214
  executorService.execute(() -> {
6966
7215
  NativeRequestContext requestContext = new NativeRequestContext(
6967
7216
  initialUrl,
@@ -6984,7 +7233,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
6984
7233
  if (_webView == null) {
6985
7234
  return;
6986
7235
  }
6987
- _webView.post(() -> loadInitialUrlDirect(initialDirectHeaders, initialMethod, httpBody));
7236
+ mainHandler.post(() -> loadInitialUrlDirect(initialDirectHeaders, initialMethod, httpBody));
6988
7237
  return;
6989
7238
  }
6990
7239
 
@@ -7000,7 +7249,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
7000
7249
  return;
7001
7250
  }
7002
7251
 
7003
- _webView.post(() -> {
7252
+ mainHandler.post(() -> {
7004
7253
  if (_webView == null) {
7005
7254
  return;
7006
7255
  }
@@ -7015,7 +7264,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
7015
7264
  ProxyRequestSupport.createNativeRequestFailureBody(initialUrl, error),
7016
7265
  StandardCharsets.UTF_8
7017
7266
  );
7018
- _webView.post(() -> {
7267
+ mainHandler.post(() -> {
7019
7268
  if (_webView == null) {
7020
7269
  return;
7021
7270
  }
@@ -7700,6 +7949,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
7700
7949
  * Update dimensions at runtime
7701
7950
  */
7702
7951
  public void updateDimensions(Integer width, Integer height, Integer x, Integer y) {
7952
+ clearFullscreen();
7703
7953
  // Update options
7704
7954
  if (width != null) {
7705
7955
  _options.setWidth(width);
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Google Material Icons: fullscreen_exit (Apache License 2.0). -->
3
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
4
+ android:width="24dp"
5
+ android:height="24dp"
6
+ android:viewportWidth="24"
7
+ android:viewportHeight="24">
8
+ <path android:fillColor="#FF000000"
9
+ android:pathData="M5,16h3v3h2v-5H5v2zM8,8H5v2h5V5H8v3zM14,19h2v-3h3v-2h-5v5zM16,8V5h-2v5h5V8h-3z" />
10
+ </vector>
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- The native exit control stays physically top-right in every layout direction. -->
3
+ <com.google.android.material.button.MaterialButton
4
+ xmlns:android="http://schemas.android.com/apk/res/android"
5
+ xmlns:app="http://schemas.android.com/apk/res-auto"
6
+ xmlns:tools="http://schemas.android.com/tools"
7
+ tools:ignore="RtlHardcoded"
8
+ style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
9
+ android:layout_width="48dp"
10
+ android:layout_height="48dp"
11
+ android:layout_gravity="top|right"
12
+ android:layout_marginTop="@dimen/fullscreen_exit_margin"
13
+ android:layout_marginRight="@dimen/fullscreen_exit_margin"
14
+ android:contentDescription="@string/exit_fullscreen"
15
+ android:insetLeft="4dp"
16
+ android:insetTop="4dp"
17
+ android:insetRight="4dp"
18
+ android:insetBottom="4dp"
19
+ android:padding="12dp"
20
+ app:icon="@drawable/ic_fullscreen_exit"
21
+ app:iconGravity="textStart"
22
+ app:iconSize="24dp"
23
+ app:iconPadding="0dp"
24
+ app:shapeAppearance="@xml/fullscreen_exit_shape" />
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!-- With the button's 4dp visual inset, this leaves an 8dp gap inside the safe area. -->
4
+ <dimen name="fullscreen_exit_margin">4dp</dimen>
5
+ <style name="FullscreenExitShape" parent="ShapeAppearance.Material3.Corner.Full" />
6
+ <style name="FullscreenExitPressedShape" parent="">
7
+ <item name="cornerFamily">rounded</item>
8
+ <item name="cornerSize">8dp</item>
9
+ </style>
10
+ </resources>
@@ -3,6 +3,7 @@
3
3
  <string name="my_string">Just a simple string</string>
4
4
  <string name="title_activity_browser">BrowserActivity</string>
5
5
  <string name="title_activity_basic">BasicActivity</string>
6
+ <string name="exit_fullscreen">Exit fullscreen</string>
6
7
  <string name="close_button">Close Button</string>
7
8
  <string name="button_near_done">Button near "done"</string>
8
9
  <string name="reload_button">Reload Button</string>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <selector xmlns:android="http://schemas.android.com/apk/res/android"
3
+ xmlns:app="http://schemas.android.com/apk/res-auto">
4
+ <item android:state_pressed="true" app:shapeAppearance="@style/FullscreenExitPressedShape" />
5
+ <item app:shapeAppearance="@style/FullscreenExitShape" />
6
+ </selector>