@capgo/capacitor-inappbrowser 8.13.3 → 8.13.5
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.
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/OrientationLayoutSupport.java +41 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/SafeAreaInsetsSupport.java +16 -5
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +270 -68
- package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +45 -4
- package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +18 -5
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package ee.forgr.capacitor_inappbrowser;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers for deciding when a managed WebView dialog must relayout after a configuration
|
|
5
|
+
* change. Capacitor activities declare {@code android:configChanges}, so the host activity
|
|
6
|
+
* (and this dialog) are not recreated on rotation — layouts and window insets must be
|
|
7
|
+
* refreshed explicitly or the WebView can stop scrolling after portrait→landscape.
|
|
8
|
+
*/
|
|
9
|
+
final class OrientationLayoutSupport {
|
|
10
|
+
|
|
11
|
+
private OrientationLayoutSupport() {}
|
|
12
|
+
|
|
13
|
+
static boolean shouldRefreshBrowserLayout(
|
|
14
|
+
Integer previousOrientation,
|
|
15
|
+
Integer previousScreenWidthDp,
|
|
16
|
+
Integer previousScreenHeightDp,
|
|
17
|
+
Integer previousSmallestScreenWidthDp,
|
|
18
|
+
Integer previousDensityDpi,
|
|
19
|
+
int currentOrientation,
|
|
20
|
+
int currentScreenWidthDp,
|
|
21
|
+
int currentScreenHeightDp,
|
|
22
|
+
int currentSmallestScreenWidthDp,
|
|
23
|
+
int currentDensityDpi
|
|
24
|
+
) {
|
|
25
|
+
if (previousOrientation == null) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
previousOrientation != currentOrientation ||
|
|
31
|
+
previousScreenWidthDp == null ||
|
|
32
|
+
previousScreenWidthDp != currentScreenWidthDp ||
|
|
33
|
+
previousScreenHeightDp == null ||
|
|
34
|
+
previousScreenHeightDp != currentScreenHeightDp ||
|
|
35
|
+
previousSmallestScreenWidthDp == null ||
|
|
36
|
+
previousSmallestScreenWidthDp != currentSmallestScreenWidthDp ||
|
|
37
|
+
previousDensityDpi == null ||
|
|
38
|
+
previousDensityDpi != currentDensityDpi
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -97,18 +97,19 @@ final class SafeAreaInsetsSupport {
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* Bottom padding to apply to the WebView container (a SwipeRefreshLayout, which honours its own
|
|
100
|
-
* padding but ignores child margins).
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* regardless of the
|
|
100
|
+
* padding but ignores child margins). Takes the larger of the safe bottom inset and the keyboard
|
|
101
|
+
* (IME) inset via {@link #resolveBottomMargin}, then adds a compensation term: on Android 15+ the
|
|
102
|
+
* AppBarLayout is pushed down by the status-bar height, which shifts the container's bottom edge
|
|
103
|
+
* off-screen by that same amount, so it must be added back as padding regardless of the options.
|
|
104
104
|
*/
|
|
105
105
|
static int resolveContainerBottomPadding(
|
|
106
106
|
boolean enabledSafeBottomMargin,
|
|
107
107
|
int safeBottomInset,
|
|
108
|
+
int imeBottom,
|
|
108
109
|
boolean appBarHandlesTopInset,
|
|
109
110
|
int statusBarTop
|
|
110
111
|
) {
|
|
111
|
-
int base = enabledSafeBottomMargin
|
|
112
|
+
int base = resolveBottomMargin(enabledSafeBottomMargin, safeBottomInset, imeBottom);
|
|
112
113
|
int appBarCompensation = appBarHandlesTopInset ? Math.max(0, statusBarTop) : 0;
|
|
113
114
|
return base + appBarCompensation;
|
|
114
115
|
}
|
|
@@ -130,4 +131,14 @@ final class SafeAreaInsetsSupport {
|
|
|
130
131
|
|
|
131
132
|
return Math.max(0, statusBarTop);
|
|
132
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Whether the WebView container must be inset for the bottom system bar. On Android 15+ the dialog
|
|
137
|
+
* is forced edge-to-edge (decorFitsSystemWindows=false), so the window always draws under the
|
|
138
|
+
* navigation bar and insetting is mandatory to keep bottom content on-screen, regardless of the
|
|
139
|
+
* enabledSafeBottomMargin opt-in that older versions honour.
|
|
140
|
+
*/
|
|
141
|
+
static boolean shouldInsetBottomForContainer(boolean enabledSafeBottomMargin, boolean isEdgeToEdge) {
|
|
142
|
+
return enabledSafeBottomMargin || isEdgeToEdge;
|
|
143
|
+
}
|
|
133
144
|
}
|
|
@@ -5,10 +5,12 @@ import android.app.Activity;
|
|
|
5
5
|
import android.app.AlertDialog;
|
|
6
6
|
import android.app.Dialog;
|
|
7
7
|
import android.content.ActivityNotFoundException;
|
|
8
|
+
import android.content.ComponentCallbacks2;
|
|
8
9
|
import android.content.Context;
|
|
9
10
|
import android.content.DialogInterface;
|
|
10
11
|
import android.content.Intent;
|
|
11
12
|
import android.content.res.AssetManager;
|
|
13
|
+
import android.content.res.Configuration;
|
|
12
14
|
import android.content.res.Resources;
|
|
13
15
|
import android.graphics.Bitmap;
|
|
14
16
|
import android.graphics.Canvas;
|
|
@@ -356,6 +358,20 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
356
358
|
private boolean cachedTitleIconResolved;
|
|
357
359
|
private boolean isHiddenModeActive = false;
|
|
358
360
|
private boolean toolbarHideInProgress = false;
|
|
361
|
+
private boolean configurationCallbacksRegistered = false;
|
|
362
|
+
private Configuration lastConfiguration;
|
|
363
|
+
private final ComponentCallbacks2 configurationCallbacks = new ComponentCallbacks2() {
|
|
364
|
+
@Override
|
|
365
|
+
public void onConfigurationChanged(Configuration newConfig) {
|
|
366
|
+
handleConfigurationChanged(newConfig);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@Override
|
|
370
|
+
public void onLowMemory() {}
|
|
371
|
+
|
|
372
|
+
@Override
|
|
373
|
+
public void onTrimMemory(int level) {}
|
|
374
|
+
};
|
|
359
375
|
private WindowManager.LayoutParams previousWindowAttributes;
|
|
360
376
|
private Drawable previousWindowBackground;
|
|
361
377
|
private ViewGroup.LayoutParams previousWebViewLayoutParams;
|
|
@@ -2824,6 +2840,9 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2824
2840
|
showAccordingToLayerModeOrFallback();
|
|
2825
2841
|
resolveOpenWebViewIfNeeded();
|
|
2826
2842
|
}
|
|
2843
|
+
|
|
2844
|
+
// Capacitor activities handle orientation themselves; refresh dialog layout explicitly.
|
|
2845
|
+
registerConfigurationCallbacks();
|
|
2827
2846
|
}
|
|
2828
2847
|
|
|
2829
2848
|
private void applyHiddenMode() {
|
|
@@ -2974,22 +2993,11 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2974
2993
|
// Check if we need Android 15+ specific fixes
|
|
2975
2994
|
boolean isAndroid15Plus = Build.VERSION.SDK_INT >= 35;
|
|
2976
2995
|
|
|
2977
|
-
// Find status bar color view and toolbar for Android 15+ specific handling
|
|
2978
|
-
View statusBarColorView = findViewById(R.id.status_bar_color_view);
|
|
2979
2996
|
View toolbarView = findViewById(R.id.tool_bar);
|
|
2980
2997
|
|
|
2981
2998
|
// Fix content browser layout height for all Android versions to allow proper scrolling
|
|
2982
2999
|
// This fixes landscape scrolling issues where bottom content is unreachable
|
|
2983
|
-
|
|
2984
|
-
if (contentBrowserLayout != null) {
|
|
2985
|
-
ViewGroup.LayoutParams layoutParams = contentBrowserLayout.getLayoutParams();
|
|
2986
|
-
if (layoutParams != null) {
|
|
2987
|
-
// Use MATCH_PARENT for height to allow proper scrolling in all orientations
|
|
2988
|
-
// The AppBarLayout's layout_behavior will handle positioning automatically
|
|
2989
|
-
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
|
|
2990
|
-
contentBrowserLayout.setLayoutParams(layoutParams);
|
|
2991
|
-
}
|
|
2992
|
-
}
|
|
3000
|
+
ensureContentBrowserMatchParentHeight();
|
|
2993
3001
|
|
|
2994
3002
|
boolean isBlankToolbar = _options != null && TextUtils.equals(_options.getToolbarType(), "blank");
|
|
2995
3003
|
if (isBlankToolbar) {
|
|
@@ -2998,53 +3006,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
2998
3006
|
|
|
2999
3007
|
// Special handling for Android 15+
|
|
3000
3008
|
if (isAndroid15Plus && !isBlankToolbar) {
|
|
3001
|
-
|
|
3002
|
-
if (toolbarView != null && toolbarView.getParent() instanceof com.google.android.material.appbar.AppBarLayout appBarLayout) {
|
|
3003
|
-
// Remove elevation to eliminate shadows (only on Android 15+)
|
|
3004
|
-
appBarLayout.setElevation(0);
|
|
3005
|
-
appBarLayout.setStateListAnimator(null);
|
|
3006
|
-
appBarLayout.setOutlineProvider(null);
|
|
3007
|
-
|
|
3008
|
-
// Determine background color to use
|
|
3009
|
-
int backgroundColor = Color.BLACK; // Default fallback
|
|
3010
|
-
if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
|
|
3011
|
-
try {
|
|
3012
|
-
backgroundColor = Color.parseColor(_options.getToolbarColor());
|
|
3013
|
-
} catch (IllegalArgumentException e) {
|
|
3014
|
-
Log.e("InAppBrowser", "Invalid toolbar color, using black: " + e.getMessage());
|
|
3015
|
-
}
|
|
3016
|
-
} else {
|
|
3017
|
-
// Follow system theme if no color specified
|
|
3018
|
-
boolean isDarkTheme = isDarkThemeEnabled();
|
|
3019
|
-
backgroundColor = isDarkTheme ? Color.BLACK : Color.WHITE;
|
|
3020
|
-
}
|
|
3021
|
-
|
|
3022
|
-
// Apply fixes for Android 15+ using a delayed post
|
|
3023
|
-
final int finalBgColor = backgroundColor;
|
|
3024
|
-
_webView.post(() -> {
|
|
3025
|
-
// Get status bar height
|
|
3026
|
-
int statusBarHeight = 0;
|
|
3027
|
-
int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
|
|
3028
|
-
if (resourceId > 0) {
|
|
3029
|
-
statusBarHeight = getContext().getResources().getDimensionPixelSize(resourceId);
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
// Fix status bar view
|
|
3033
|
-
if (statusBarColorView != null) {
|
|
3034
|
-
ViewGroup.LayoutParams params = statusBarColorView.getLayoutParams();
|
|
3035
|
-
params.height = statusBarHeight;
|
|
3036
|
-
statusBarColorView.setLayoutParams(params);
|
|
3037
|
-
statusBarColorView.setBackgroundColor(finalBgColor);
|
|
3038
|
-
statusBarColorView.setVisibility(View.VISIBLE);
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
// Fix AppBarLayout position
|
|
3042
|
-
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) appBarLayout.getLayoutParams();
|
|
3043
|
-
params.topMargin = statusBarHeight;
|
|
3044
|
-
appBarLayout.setLayoutParams(params);
|
|
3045
|
-
appBarLayout.setBackgroundColor(finalBgColor);
|
|
3046
|
-
});
|
|
3047
|
-
}
|
|
3009
|
+
refreshEdgeToEdgeChrome();
|
|
3048
3010
|
}
|
|
3049
3011
|
|
|
3050
3012
|
// Resolve insets from the dialog window root; layout children can receive already-fitted zero insets.
|
|
@@ -3152,6 +3114,232 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
3152
3114
|
mainHandler.postDelayed(this::applyContainerInsetsSnapshot, 1200);
|
|
3153
3115
|
}
|
|
3154
3116
|
|
|
3117
|
+
private void registerConfigurationCallbacks() {
|
|
3118
|
+
if (configurationCallbacksRegistered) {
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
Context callbackContext = activity != null ? activity : _context;
|
|
3123
|
+
if (callbackContext == null) {
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
lastConfiguration = new Configuration(callbackContext.getResources().getConfiguration());
|
|
3128
|
+
callbackContext.registerComponentCallbacks(configurationCallbacks);
|
|
3129
|
+
configurationCallbacksRegistered = true;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
private void unregisterConfigurationCallbacks() {
|
|
3133
|
+
if (!configurationCallbacksRegistered) {
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
Context callbackContext = activity != null ? activity : _context;
|
|
3138
|
+
if (callbackContext != null) {
|
|
3139
|
+
try {
|
|
3140
|
+
callbackContext.unregisterComponentCallbacks(configurationCallbacks);
|
|
3141
|
+
} catch (Exception e) {
|
|
3142
|
+
Log.w("InAppBrowser", "Failed to unregister configuration callbacks: " + e.getMessage());
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
configurationCallbacksRegistered = false;
|
|
3147
|
+
lastConfiguration = null;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
private void handleConfigurationChanged(Configuration newConfig) {
|
|
3151
|
+
if (isDismissing || _webView == null) {
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
Integer previousOrientation = lastConfiguration != null ? lastConfiguration.orientation : null;
|
|
3156
|
+
Integer previousScreenWidthDp = lastConfiguration != null ? lastConfiguration.screenWidthDp : null;
|
|
3157
|
+
Integer previousScreenHeightDp = lastConfiguration != null ? lastConfiguration.screenHeightDp : null;
|
|
3158
|
+
Integer previousSmallestScreenWidthDp = lastConfiguration != null ? lastConfiguration.smallestScreenWidthDp : null;
|
|
3159
|
+
Integer previousDensityDpi = lastConfiguration != null ? lastConfiguration.densityDpi : null;
|
|
3160
|
+
|
|
3161
|
+
int currentOrientation = newConfig != null ? newConfig.orientation : (previousOrientation != null ? previousOrientation : 0);
|
|
3162
|
+
int currentScreenWidthDp =
|
|
3163
|
+
newConfig != null ? newConfig.screenWidthDp : (previousScreenWidthDp != null ? previousScreenWidthDp : 0);
|
|
3164
|
+
int currentScreenHeightDp =
|
|
3165
|
+
newConfig != null ? newConfig.screenHeightDp : (previousScreenHeightDp != null ? previousScreenHeightDp : 0);
|
|
3166
|
+
int currentSmallestScreenWidthDp =
|
|
3167
|
+
newConfig != null
|
|
3168
|
+
? newConfig.smallestScreenWidthDp
|
|
3169
|
+
: (previousSmallestScreenWidthDp != null ? previousSmallestScreenWidthDp : 0);
|
|
3170
|
+
int currentDensityDpi = newConfig != null ? newConfig.densityDpi : (previousDensityDpi != null ? previousDensityDpi : 0);
|
|
3171
|
+
|
|
3172
|
+
boolean shouldRefresh = OrientationLayoutSupport.shouldRefreshBrowserLayout(
|
|
3173
|
+
previousOrientation,
|
|
3174
|
+
previousScreenWidthDp,
|
|
3175
|
+
previousScreenHeightDp,
|
|
3176
|
+
previousSmallestScreenWidthDp,
|
|
3177
|
+
previousDensityDpi,
|
|
3178
|
+
currentOrientation,
|
|
3179
|
+
currentScreenWidthDp,
|
|
3180
|
+
currentScreenHeightDp,
|
|
3181
|
+
currentSmallestScreenWidthDp,
|
|
3182
|
+
currentDensityDpi
|
|
3183
|
+
);
|
|
3184
|
+
|
|
3185
|
+
if (newConfig != null) {
|
|
3186
|
+
lastConfiguration = new Configuration(newConfig);
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
if (!shouldRefresh) {
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
3194
|
+
refreshLayoutForConfigurationChange();
|
|
3195
|
+
} else {
|
|
3196
|
+
mainHandler.post(this::refreshLayoutForConfigurationChange);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
/**
|
|
3201
|
+
* Capacitor keeps the host activity alive across orientation changes. Re-apply fullscreen
|
|
3202
|
+
* window dimensions, system chrome, and safe-area insets so the WebView stays scrollable
|
|
3203
|
+
* after portrait↔landscape transitions.
|
|
3204
|
+
*/
|
|
3205
|
+
private void refreshLayoutForConfigurationChange() {
|
|
3206
|
+
if (isDismissing || _webView == null) {
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
if (isHiddenModeActive) {
|
|
3211
|
+
requestSafeAreaInsets();
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
if (backLayerActive) {
|
|
3216
|
+
applyBackLayerDimensions();
|
|
3217
|
+
} else {
|
|
3218
|
+
applyDimensions();
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
ensureContentBrowserMatchParentHeight();
|
|
3222
|
+
|
|
3223
|
+
boolean isBlankToolbar = _options != null && TextUtils.equals(_options.getToolbarType(), "blank");
|
|
3224
|
+
if (isBlankToolbar) {
|
|
3225
|
+
configureBlankToolbarLayout();
|
|
3226
|
+
} else if (Build.VERSION.SDK_INT >= 35) {
|
|
3227
|
+
refreshEdgeToEdgeChrome();
|
|
3228
|
+
} else {
|
|
3229
|
+
refreshStatusBarColorViewHeight();
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
View coordinator = findViewById(R.id.coordinator_layout);
|
|
3233
|
+
if (coordinator != null) {
|
|
3234
|
+
coordinator.requestLayout();
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
if (swipeRefreshLayout != null) {
|
|
3238
|
+
swipeRefreshLayout.requestLayout();
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
_webView.requestLayout();
|
|
3242
|
+
_webView.invalidate();
|
|
3243
|
+
requestSafeAreaInsets();
|
|
3244
|
+
|
|
3245
|
+
mainHandler.post(this::applyContainerInsetsSnapshot);
|
|
3246
|
+
mainHandler.postDelayed(this::applyContainerInsetsSnapshot, 100);
|
|
3247
|
+
mainHandler.postDelayed(this::applyContainerInsetsSnapshot, 300);
|
|
3248
|
+
|
|
3249
|
+
_webView.post(() -> {
|
|
3250
|
+
if (_webView == null) {
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
_webView.evaluateJavascript("(function(){window.dispatchEvent(new Event('resize'));})();", null);
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
private void ensureContentBrowserMatchParentHeight() {
|
|
3258
|
+
View contentBrowserLayout = findViewById(R.id.content_browser_layout);
|
|
3259
|
+
if (contentBrowserLayout == null) {
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
ViewGroup.LayoutParams layoutParams = contentBrowserLayout.getLayoutParams();
|
|
3264
|
+
if (layoutParams == null) {
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
|
|
3269
|
+
contentBrowserLayout.setLayoutParams(layoutParams);
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
private void refreshStatusBarColorViewHeight() {
|
|
3273
|
+
View statusBarColorView = findViewById(R.id.status_bar_color_view);
|
|
3274
|
+
if (statusBarColorView == null) {
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
int statusBarHeight = getSystemStatusBarHeight();
|
|
3279
|
+
ViewGroup.LayoutParams params = statusBarColorView.getLayoutParams();
|
|
3280
|
+
if (params == null) {
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
params.height = statusBarHeight;
|
|
3285
|
+
statusBarColorView.setLayoutParams(params);
|
|
3286
|
+
statusBarColorView.requestLayout();
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
private void refreshEdgeToEdgeChrome() {
|
|
3290
|
+
if (_webView == null || _options == null) {
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
View statusBarColorView = findViewById(R.id.status_bar_color_view);
|
|
3295
|
+
View toolbarView = findViewById(R.id.tool_bar);
|
|
3296
|
+
if (toolbarView == null || !(toolbarView.getParent() instanceof com.google.android.material.appbar.AppBarLayout appBarLayout)) {
|
|
3297
|
+
refreshStatusBarColorViewHeight();
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
appBarLayout.setElevation(0);
|
|
3302
|
+
appBarLayout.setStateListAnimator(null);
|
|
3303
|
+
appBarLayout.setOutlineProvider(null);
|
|
3304
|
+
|
|
3305
|
+
int backgroundColor = Color.BLACK;
|
|
3306
|
+
if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
|
|
3307
|
+
try {
|
|
3308
|
+
backgroundColor = Color.parseColor(_options.getToolbarColor());
|
|
3309
|
+
} catch (IllegalArgumentException e) {
|
|
3310
|
+
Log.e("InAppBrowser", "Invalid toolbar color, using black: " + e.getMessage());
|
|
3311
|
+
}
|
|
3312
|
+
} else {
|
|
3313
|
+
backgroundColor = isDarkThemeEnabled() ? Color.BLACK : Color.WHITE;
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
final int finalBgColor = backgroundColor;
|
|
3317
|
+
_webView.post(() -> {
|
|
3318
|
+
if (_webView == null) {
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
int statusBarHeight = getSystemStatusBarHeight();
|
|
3323
|
+
|
|
3324
|
+
if (statusBarColorView != null) {
|
|
3325
|
+
ViewGroup.LayoutParams params = statusBarColorView.getLayoutParams();
|
|
3326
|
+
if (params != null) {
|
|
3327
|
+
params.height = statusBarHeight;
|
|
3328
|
+
statusBarColorView.setLayoutParams(params);
|
|
3329
|
+
}
|
|
3330
|
+
statusBarColorView.setBackgroundColor(finalBgColor);
|
|
3331
|
+
statusBarColorView.setVisibility(View.VISIBLE);
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
ViewGroup.LayoutParams appBarParams = appBarLayout.getLayoutParams();
|
|
3335
|
+
if (appBarParams instanceof ViewGroup.MarginLayoutParams marginParams) {
|
|
3336
|
+
marginParams.topMargin = statusBarHeight;
|
|
3337
|
+
appBarLayout.setLayoutParams(marginParams);
|
|
3338
|
+
}
|
|
3339
|
+
appBarLayout.setBackgroundColor(finalBgColor);
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3155
3343
|
private void applySafeAreaMargins(
|
|
3156
3344
|
Insets bars,
|
|
3157
3345
|
Insets navigationBars,
|
|
@@ -3227,6 +3415,8 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
3227
3415
|
Insets systemGestures = windowInsets != null ? windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures()) : Insets.NONE;
|
|
3228
3416
|
Insets mandatoryGestures =
|
|
3229
3417
|
windowInsets != null ? windowInsets.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures()) : Insets.NONE;
|
|
3418
|
+
Insets ime = windowInsets != null ? windowInsets.getInsets(WindowInsetsCompat.Type.ime()) : Insets.NONE;
|
|
3419
|
+
boolean keyboardVisible = windowInsets != null && windowInsets.isVisible(WindowInsetsCompat.Type.ime());
|
|
3230
3420
|
|
|
3231
3421
|
boolean isAndroid15Plus = Build.VERSION.SDK_INT >= 35;
|
|
3232
3422
|
View toolbarView = findViewById(R.id.tool_bar);
|
|
@@ -3237,8 +3427,10 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
3237
3427
|
toolbarView.getVisibility() == View.VISIBLE &&
|
|
3238
3428
|
toolbarView.getParent() instanceof com.google.android.material.appbar.AppBarLayout;
|
|
3239
3429
|
|
|
3430
|
+
boolean applyBottomInset = SafeAreaInsetsSupport.shouldInsetBottomForContainer(_options.getEnabledSafeMargin(), isAndroid15Plus);
|
|
3431
|
+
|
|
3240
3432
|
int statusBarTop = bars.top > 0 ? bars.top : getSystemStatusBarHeight();
|
|
3241
|
-
int fallbackBottomInset =
|
|
3433
|
+
int fallbackBottomInset = applyBottomInset ? getSystemNavigationBarHeight() : 0;
|
|
3242
3434
|
int safeBottomInset = SafeAreaInsetsSupport.resolveSafeBottomInsetWithFallback(
|
|
3243
3435
|
bars.bottom,
|
|
3244
3436
|
navigationBars.bottom,
|
|
@@ -3249,7 +3441,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
3249
3441
|
navigationBars.left,
|
|
3250
3442
|
navigationBars.right,
|
|
3251
3443
|
fallbackBottomInset,
|
|
3252
|
-
|
|
3444
|
+
applyBottomInset
|
|
3253
3445
|
);
|
|
3254
3446
|
|
|
3255
3447
|
int padTop = SafeAreaInsetsSupport.resolveContainerTopPadding(
|
|
@@ -3258,9 +3450,11 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
3258
3450
|
statusBarTop,
|
|
3259
3451
|
appBarHandlesTopInset
|
|
3260
3452
|
);
|
|
3453
|
+
int imeBottom = SafeAreaInsetsSupport.resolveImeBottomInset(keyboardVisible, ime.bottom, isAndroid15Plus);
|
|
3261
3454
|
int padBottom = SafeAreaInsetsSupport.resolveContainerBottomPadding(
|
|
3262
|
-
|
|
3455
|
+
applyBottomInset,
|
|
3263
3456
|
safeBottomInset,
|
|
3457
|
+
imeBottom,
|
|
3264
3458
|
appBarHandlesTopInset,
|
|
3265
3459
|
statusBarTop
|
|
3266
3460
|
);
|
|
@@ -5853,6 +6047,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
5853
6047
|
|
|
5854
6048
|
@Override
|
|
5855
6049
|
public void dismiss() {
|
|
6050
|
+
unregisterConfigurationCallbacks();
|
|
5856
6051
|
scheduleHostWebViewInsetRestore();
|
|
5857
6052
|
detachBackLayer();
|
|
5858
6053
|
|
|
@@ -6963,7 +7158,12 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
6963
7158
|
Integer x = _options.getX();
|
|
6964
7159
|
Integer y = _options.getY();
|
|
6965
7160
|
|
|
6966
|
-
|
|
7161
|
+
Window window = getWindow();
|
|
7162
|
+
if (window == null) {
|
|
7163
|
+
return;
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
WindowManager.LayoutParams params = window.getAttributes();
|
|
6967
7167
|
|
|
6968
7168
|
// If both width and height are specified, use custom dimensions
|
|
6969
7169
|
if (width != null && height != null) {
|
|
@@ -6973,6 +7173,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
6973
7173
|
params.x = (x != null) ? (int) getPixels(x) : 0;
|
|
6974
7174
|
params.y = (y != null) ? (int) getPixels(y) : 0;
|
|
6975
7175
|
params.gravity = Gravity.TOP | Gravity.START;
|
|
7176
|
+
window.setAttributes(params);
|
|
6976
7177
|
} else if (height != null && width == null) {
|
|
6977
7178
|
// If only height is specified, use custom height with fullscreen width
|
|
6978
7179
|
params.gravity = Gravity.TOP | Gravity.LEFT;
|
|
@@ -6981,15 +7182,16 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
|
|
|
6981
7182
|
params.x = 0;
|
|
6982
7183
|
params.y = (y != null) ? (int) getPixels(y) : 0;
|
|
6983
7184
|
params.gravity = Gravity.TOP | Gravity.START;
|
|
7185
|
+
window.setAttributes(params);
|
|
6984
7186
|
} else {
|
|
6985
|
-
// Default to fullscreen
|
|
6986
|
-
|
|
6987
|
-
params.height = WindowManager.LayoutParams.MATCH_PARENT;
|
|
7187
|
+
// Default to fullscreen. Prefer setLayout so MATCH_PARENT is not replaced by
|
|
7188
|
+
// previously resolved portrait/landscape pixel sizes after a configuration change.
|
|
6988
7189
|
params.x = 0;
|
|
6989
7190
|
params.y = 0;
|
|
7191
|
+
params.gravity = Gravity.TOP | Gravity.START;
|
|
7192
|
+
window.setAttributes(params);
|
|
7193
|
+
window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
|
|
6990
7194
|
}
|
|
6991
|
-
|
|
6992
|
-
getWindow().setAttributes(params);
|
|
6993
7195
|
}
|
|
6994
7196
|
|
|
6995
7197
|
/**
|
|
@@ -110,6 +110,42 @@ enum CustomSchemeOpenSupport {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
enum SecureWindowRedirectSupport {
|
|
114
|
+
/// The auth session reports any navigation on the callback scheme, so the redirect is
|
|
115
|
+
/// identified component-wise. Query items configured in the redirect URI are matched,
|
|
116
|
+
/// while any extra items (e.g. provider code, state) are ignored, unless they reuse a
|
|
117
|
+
/// configured name: parsers disagree over which duplicate takes precedence.
|
|
118
|
+
static func matches(_ callbackURL: URL, redirectUri: String) -> Bool {
|
|
119
|
+
guard let expected = URLComponents(string: redirectUri),
|
|
120
|
+
let received = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
|
|
121
|
+
received.scheme?.lowercased() == expected.scheme?.lowercased(),
|
|
122
|
+
received.percentEncodedUser == expected.percentEncodedUser,
|
|
123
|
+
received.percentEncodedPassword == expected.percentEncodedPassword,
|
|
124
|
+
received.percentEncodedHost?.lowercased() == expected.percentEncodedHost?.lowercased(),
|
|
125
|
+
received.port == expected.port,
|
|
126
|
+
normalizedPath(received.percentEncodedPath) == normalizedPath(expected.percentEncodedPath) else {
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let expectedItems = expected.percentEncodedQueryItems ?? []
|
|
131
|
+
let receivedItems = received.percentEncodedQueryItems ?? []
|
|
132
|
+
let configuredDecodedNames = Set((expected.queryItems ?? []).map(\.name))
|
|
133
|
+
let configuredReceivedItemCount = (received.queryItems ?? []).filter {
|
|
134
|
+
configuredDecodedNames.contains($0.name)
|
|
135
|
+
}.count
|
|
136
|
+
guard configuredReceivedItemCount == expectedItems.count else {
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
return Set(expectedItems.map(\.name)).allSatisfy { name in
|
|
140
|
+
receivedItems.filter { $0.name == name } == expectedItems.filter { $0.name == name }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private static func normalizedPath(_ path: String) -> String {
|
|
145
|
+
path == "/" ? "" : path
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
113
149
|
protocol ProxyRequestLocating {
|
|
114
150
|
func hasPendingProxyRequest(_ requestId: String) -> Bool
|
|
115
151
|
}
|
|
@@ -376,7 +412,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
376
412
|
case aware = "AWARE"
|
|
377
413
|
case fakeVisible = "FAKE_VISIBLE"
|
|
378
414
|
}
|
|
379
|
-
private let pluginVersion: String = "8.13.
|
|
415
|
+
private let pluginVersion: String = "8.13.5"
|
|
380
416
|
public let identifier = "CapgoInAppBrowserPlugin"
|
|
381
417
|
public let jsName = "CapgoInAppBrowser"
|
|
382
418
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -1268,7 +1304,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1268
1304
|
return
|
|
1269
1305
|
}
|
|
1270
1306
|
let titleFontFamily = call.getString("titleFontFamily")
|
|
1271
|
-
let isInspectable = call.getBool("isInspectable", false)
|
|
1307
|
+
let isInspectable = call.getBool("isInspectable", self.bridge?.config.isWebDebuggable ?? false)
|
|
1272
1308
|
let preventDeeplink = call.getBool("preventDeeplink", false)
|
|
1273
1309
|
let openBlankTargetInWebView = call.getBool("openBlankTargetInWebView", false)
|
|
1274
1310
|
let isAnimated = call.getBool("isAnimated", true)
|
|
@@ -2637,6 +2673,11 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2637
2673
|
return
|
|
2638
2674
|
}
|
|
2639
2675
|
|
|
2676
|
+
guard let callbackURLScheme = URL(string: redirectUri)?.scheme else {
|
|
2677
|
+
call.reject("Invalid Redirect URI")
|
|
2678
|
+
return
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2640
2681
|
// Store the call for later resolution
|
|
2641
2682
|
self.openSecureWindowCall = call
|
|
2642
2683
|
|
|
@@ -2644,7 +2685,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2644
2685
|
|
|
2645
2686
|
// Open the URL in a secure browser window
|
|
2646
2687
|
DispatchQueue.main.async {
|
|
2647
|
-
let session = ASWebAuthenticationSession(url: url, callbackURLScheme:
|
|
2688
|
+
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme) {
|
|
2648
2689
|
callbackURL, error in
|
|
2649
2690
|
|
|
2650
2691
|
// Clean up the stored call
|
|
@@ -2661,7 +2702,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2661
2702
|
return
|
|
2662
2703
|
}
|
|
2663
2704
|
|
|
2664
|
-
if !
|
|
2705
|
+
if !SecureWindowRedirectSupport.matches(callbackURL, redirectUri: redirectUri) {
|
|
2665
2706
|
call.reject("Redirect URI does not match, expected " + redirectUri + " but got " + callbackURL.absoluteString)
|
|
2666
2707
|
return
|
|
2667
2708
|
}
|
|
@@ -439,6 +439,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
439
439
|
open var httpMethod: String?
|
|
440
440
|
open var httpBody: String?
|
|
441
441
|
open var capBrowserPlugin: CapgoInAppBrowserPlugin?
|
|
442
|
+
open var isInspectable: Bool = false
|
|
442
443
|
var instanceId: String = ""
|
|
443
444
|
var shareDisclaimer: [String: Any]?
|
|
444
445
|
var shareSubject: String?
|
|
@@ -2032,11 +2033,12 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
2032
2033
|
}
|
|
2033
2034
|
}
|
|
2034
2035
|
|
|
2035
|
-
open func initWebview(isInspectable: Bool =
|
|
2036
|
+
open func initWebview(isInspectable: Bool = false) {
|
|
2036
2037
|
if self.isWebViewInitialized {
|
|
2037
2038
|
return
|
|
2038
2039
|
}
|
|
2039
2040
|
self.isWebViewInitialized = true
|
|
2041
|
+
self.isInspectable = isInspectable
|
|
2040
2042
|
self.startObservingKeyboardViewportChanges()
|
|
2041
2043
|
self.view.backgroundColor = UIColor.white
|
|
2042
2044
|
|
|
@@ -2183,9 +2185,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
2183
2185
|
// }
|
|
2184
2186
|
|
|
2185
2187
|
if #available(iOS 16.4, *) {
|
|
2186
|
-
webView.isInspectable =
|
|
2187
|
-
} else {
|
|
2188
|
-
// Fallback on earlier versions
|
|
2188
|
+
webView.isInspectable = isInspectable
|
|
2189
2189
|
}
|
|
2190
2190
|
|
|
2191
2191
|
// First add the webView to view hierarchy
|
|
@@ -2341,7 +2341,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
2341
2341
|
self.view.backgroundColor = parent.view.backgroundColor
|
|
2342
2342
|
self.title = parent.title ?? request.url?.host ?? "Popup Window"
|
|
2343
2343
|
self.navigationItem.title = self.title
|
|
2344
|
-
self.initWebview()
|
|
2344
|
+
self.initWebview(isInspectable: parent.isInspectable)
|
|
2345
2345
|
return self.capableWebView
|
|
2346
2346
|
}
|
|
2347
2347
|
|
|
@@ -3338,6 +3338,19 @@ extension WKWebViewController: WKUIDelegate {
|
|
|
3338
3338
|
// This allows websites to access location when opened with openWebView
|
|
3339
3339
|
decisionHandler(.grant)
|
|
3340
3340
|
}
|
|
3341
|
+
|
|
3342
|
+
@available(iOS 15.0, *)
|
|
3343
|
+
public func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
|
|
3344
|
+
print("[InAppBrowser] Media capture permission requested for origin: \(origin.host)")
|
|
3345
|
+
|
|
3346
|
+
// Grant media capture permission automatically, matching Capacitor core's
|
|
3347
|
+
// WebViewDelegationHandler. The OS-level camera/microphone permission
|
|
3348
|
+
// (Info.plist usage description + system prompt) still applies — this only
|
|
3349
|
+
// suppresses WebKit's extra per-origin prompt, which is not persisted
|
|
3350
|
+
// across app launches (WebKit bug 220416) and would otherwise re-prompt
|
|
3351
|
+
// users on every cold start.
|
|
3352
|
+
decisionHandler(.grant)
|
|
3353
|
+
}
|
|
3341
3354
|
}
|
|
3342
3355
|
|
|
3343
3356
|
// MARK: - Host Blocking Utilities
|