Skip to content

Commit c740729

Browse files
Capacitor+ Botriderx
andcommitted
fix: sync upstream PR ionic-team#8535 systembars/safe area changes only
Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
1 parent 8687b31 commit c740729

7 files changed

Lines changed: 105 additions & 203 deletions

File tree

android/capacitor/src/main/assets/native-bridge.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -417,12 +417,6 @@ var nativeBridge = (function (exports) {
417417
}
418418
};
419419
const platform = getPlatformId(win);
420-
if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
421-
// add DOM ready listener for System Bars
422-
document.addEventListener('DOMContentLoaded', function () {
423-
win.CapacitorSystemBarsAndroidInterface.onDOMReady();
424-
});
425-
}
426420
if (platform == 'android' || platform == 'ios') {
427421
// patch document.cookie on Android/iOS
428422
win.CapacitorCookiesDescriptor =

android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java

Lines changed: 74 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
package com.getcapacitor.plugin;
22

3-
import android.annotation.SuppressLint;
43
import android.content.Context;
54
import android.content.pm.PackageInfo;
65
import android.content.res.Configuration;
76
import android.content.res.Resources;
8-
import android.os.Build;
97
import android.util.TypedValue;
108
import android.view.View;
119
import android.view.Window;
12-
import android.webkit.JavascriptInterface;
1310
import android.webkit.WebView;
1411
import androidx.core.graphics.Insets;
1512
import androidx.core.view.ViewCompat;
@@ -34,9 +31,9 @@ public class SystemBars extends Plugin {
3431
static final String BAR_STATUS_BAR = "StatusBar";
3532
static final String BAR_GESTURE_BAR = "NavigationBar";
3633

37-
// TODO: In Cap 9, add an additional option "full"
3834
static final String INSETS_HANDLING_CSS = "css";
3935
static final String INSETS_HANDLING_DISABLE = "disable";
36+
static final String INSETS_HANDLING_NATIVE = "native";
4037

4138
// https://issues.chromium.org/issues/40699457
4239
private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140;
@@ -62,11 +59,22 @@ function capacitorSystemBarsCheckMetaViewport() {
6259
private String currentStatusBarStyle = STYLE_DEFAULT;
6360
private String currentGestureBarStyle = STYLE_DEFAULT;
6461

65-
private boolean navBarVisible = true;
62+
// Declare variable at this scope to help prevent adding multiple listeners.
63+
private WebViewListener webViewListener;
64+
65+
private void warnAboutUnsupportedConfigurationValues() {
66+
boolean keyboardResizeOnFullScreen = bridge.getConfig().getPluginConfiguration("Keyboard").getBoolean("resizeOnFullScreen", false);
67+
68+
if (!INSETS_HANDLING_DISABLE.equals(insetsHandling) && keyboardResizeOnFullScreen) {
69+
Logger.warn(
70+
"SystemBars",
71+
"You should omit `Keyboard.resizeOnFullScreen` in your `capacitor.config.json`. Other values can lead to unexpected behavior."
72+
);
73+
}
74+
}
6675

6776
@Override
6877
public void load() {
69-
getBridge().getWebView().addJavascriptInterface(this, "CapacitorSystemBarsAndroidInterface");
7078
super.load();
7179

7280
initSystemBars();
@@ -76,23 +84,27 @@ public void load() {
7684
protected void handleOnStart() {
7785
super.handleOnStart();
7886

79-
this.getBridge().addWebViewListener(
80-
new WebViewListener() {
87+
if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
88+
return;
89+
}
90+
91+
if (webViewListener == null) {
92+
webViewListener = new WebViewListener() {
8193
@Override
8294
public void onPageCommitVisible(WebView view, String url) {
8395
super.onPageCommitVisible(view, url);
84-
View parentView = (View) getBridge().getWebView().getParent();
85-
ViewCompat.requestApplyInsets(parentView);
86-
if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
87-
WindowInsetsCompat rootInsets = ViewCompat.getRootWindowInsets(parentView);
88-
if (rootInsets != null) {
89-
Insets safeArea = calcSafeAreaInsets(rootInsets);
90-
injectSafeAreaCSS(safeArea.top, safeArea.right, safeArea.bottom, safeArea.left);
91-
}
92-
}
96+
bridge
97+
.getWebView()
98+
.evaluateJavascript(viewportMetaJSFunction, (res) -> {
99+
hasViewportCover = res.equals("true");
100+
101+
// Request new execution tree of `setOnApplyWindowInsetsListener`
102+
bridge.getWebView().requestApplyInsets();
103+
});
93104
}
94-
}
95-
);
105+
};
106+
this.getBridge().addWebViewListener(webViewListener);
107+
}
96108
}
97109

98110
@Override
@@ -104,11 +116,20 @@ protected void handleOnConfigurationChanged(Configuration newConfig) {
104116
}
105117

106118
private void initSystemBars() {
119+
// If you already know what the value of the `viewport-fit=` meta tag is going to be,
120+
// passing it here through `initialViewportFitValueHint` can help prevent layout shifting.
121+
String configuredInitialViewportFitValueHint = getConfig().getString("initialViewportFitValueHint", "");
122+
hasViewportCover = "cover".equals(configuredInitialViewportFitValueHint);
123+
107124
String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase(Locale.US);
108125
boolean hidden = getConfig().getBoolean("hidden", false);
109126

110127
String configuredInsetsHandling = getConfig().getString("insetsHandling", INSETS_HANDLING_CSS);
111-
if (INSETS_HANDLING_CSS.equals(configuredInsetsHandling) || INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling)) {
128+
if (
129+
INSETS_HANDLING_CSS.equals(configuredInsetsHandling) ||
130+
INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling) ||
131+
INSETS_HANDLING_NATIVE.equals(configuredInsetsHandling)
132+
) {
112133
insetsHandling = configuredInsetsHandling;
113134
} else {
114135
Logger.warn(
@@ -118,21 +139,13 @@ private void initSystemBars() {
118139
insetsHandling = INSETS_HANDLING_CSS;
119140
}
120141

142+
warnAboutUnsupportedConfigurationValues();
143+
121144
initWindowInsetsListener();
122-
initSafeAreaCSSVariables();
123145

124146
getBridge().executeOnMainThread(() -> {
125-
Window window = getActivity().getWindow();
126-
WindowCompat.setDecorFitsSystemWindows(window, false);
127-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
128-
window.setNavigationBarColor(android.graphics.Color.TRANSPARENT);
129-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
130-
window.setNavigationBarContrastEnforced(false);
131-
}
132-
}
133147
setStyle(style, "");
134148
setHidden(hidden, "");
135-
ViewCompat.requestApplyInsets((View) getBridge().getWebView().getParent());
136149
});
137150
}
138151

@@ -172,96 +185,14 @@ public void setAnimation(final PluginCall call) {
172185
call.resolve();
173186
}
174187

175-
@Override
176-
protected void handleOnResume() {
177-
super.handleOnResume();
178-
getBridge().executeOnMainThread(() -> {
179-
Window window = getActivity().getWindow();
180-
WindowCompat.setDecorFitsSystemWindows(window, false);
181-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
182-
window.setNavigationBarColor(android.graphics.Color.TRANSPARENT);
183-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
184-
window.setNavigationBarContrastEnforced(false);
185-
}
186-
}
187-
setStyle(currentGestureBarStyle, BAR_GESTURE_BAR);
188-
setStyle(currentStatusBarStyle, BAR_STATUS_BAR);
189-
ViewCompat.requestApplyInsets((View) getBridge().getWebView().getParent());
190-
});
191-
}
192-
193-
@JavascriptInterface
194-
public void onDOMReady() {
195-
if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
196-
getActivity().runOnUiThread(() -> {
197-
this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
198-
hasViewportCover = res.equals("true");
199-
200-
getBridge().getWebView().requestApplyInsets();
201-
});
202-
});
203-
}
204-
}
205-
206-
private Insets calcSafeAreaInsets(WindowInsetsCompat insets) {
207-
Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
208-
209-
int bottom = safeArea.bottom;
210-
211-
if (bottom == 0 && Build.VERSION.SDK_INT < Build.VERSION_CODES.R && safeArea.left == 0 && safeArea.right == 0) {
212-
// skip if nav bar is on a side (landscape)
213-
bottom = getNavBarHeightFromResources();
214-
}
215-
216-
boolean imeVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
217-
? insets.isVisible(WindowInsetsCompat.Type.ime())
218-
: insets.getInsets(WindowInsetsCompat.Type.ime()).bottom > 0;
219-
220-
if (imeVisible) {
221-
return Insets.of(safeArea.left, safeArea.top, safeArea.right, 0);
222-
}
223-
return Insets.of(safeArea.left, safeArea.top, safeArea.right, bottom);
224-
}
225-
226-
// Only reached on API < 30 when the window reports no inset on any edge, so the
227-
// choice is this approximation or a hardcoded 0. Lint rejects the internal
228-
// resource because OEMs may change it; a wrong height still beats no inset.
229-
@SuppressLint({ "InternalInsetResource", "DiscouragedApi" })
230-
private int getNavBarHeightFromResources() {
231-
if (!navBarVisible) return 0;
232-
android.content.res.Resources res = getActivity().getResources();
233-
int heightId = res.getIdentifier("navigation_bar_height", "dimen", "android");
234-
return heightId > 0 ? res.getDimensionPixelSize(heightId) : 0;
235-
}
236-
237-
private void initSafeAreaCSSVariables() {
238-
if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
239-
WindowInsetsCompat insets;
240-
241-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
242-
View v = (View) this.getBridge().getWebView().getParent();
243-
insets = ViewCompat.getRootWindowInsets(v);
244-
} else {
245-
insets = WindowInsetsCompat.CONSUMED;
246-
}
247-
248-
if (insets != null) {
249-
Insets safeAreaInsets = calcSafeAreaInsets(insets);
250-
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
251-
}
252-
}
253-
}
254-
255188
private void initWindowInsetsListener() {
256189
if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
257190
return;
258191
}
259192

260-
ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> {
261-
// getRootWindowInsets() bypasses AppCompat's intermediate view consuming the bottom inset on API < 30
262-
WindowInsetsCompat rawInsets = ViewCompat.getRootWindowInsets(v);
263-
WindowInsetsCompat safeAreaSource = (rawInsets != null) ? rawInsets : insets;
193+
View view = getActivity().getWindow().getDecorView();
264194

195+
ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
265196
boolean shouldPassthroughInsets = getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover;
266197

267198
Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
@@ -272,10 +203,7 @@ private void initWindowInsetsListener() {
272203
// We need to correct for a possible shown IME
273204
v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0);
274205

275-
Insets safeAreaInsets = calcSafeAreaInsets(safeAreaSource);
276-
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
277-
278-
return new WindowInsetsCompat.Builder(insets)
206+
WindowInsetsCompat newInsets = new WindowInsetsCompat.Builder(insets)
279207
.setInsets(
280208
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(),
281209
Insets.of(
@@ -286,9 +214,19 @@ private void initWindowInsetsListener() {
286214
)
287215
)
288216
.build();
217+
218+
injectSafeAreaCSS(newInsets);
219+
220+
return newInsets;
289221
}
290222

291-
v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0);
223+
// We need to correct for a possible shown IME
224+
v.setPadding(
225+
systemBarsInsets.left,
226+
systemBarsInsets.top,
227+
systemBarsInsets.right,
228+
keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom
229+
);
292230

293231
// Returning `WindowInsetsCompat.CONSUMED` breaks recalculation of safe area insets
294232
// So we have to explicitly set insets to `0`
@@ -297,20 +235,28 @@ private void initWindowInsetsListener() {
297235
.setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0))
298236
.build();
299237

300-
Insets safeAreaInsets = calcSafeAreaInsets(safeAreaSource);
301-
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
238+
injectSafeAreaCSS(newInsets);
302239

303240
return newInsets;
304241
});
305242
}
306243

307-
private void injectSafeAreaCSS(int top, int right, int bottom, int left) {
244+
private void injectSafeAreaCSS(WindowInsetsCompat insets) {
245+
if (!INSETS_HANDLING_CSS.equals(insetsHandling)) {
246+
return;
247+
}
248+
249+
Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
250+
boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
251+
308252
// Convert pixels to density-independent pixels
309253
float density = getActivity().getResources().getDisplayMetrics().density;
310-
float topPx = top / density;
311-
float rightPx = right / density;
312-
float bottomPx = bottom / density;
313-
float leftPx = left / density;
254+
float topPx = systemBarsInsets.top / density;
255+
float rightPx = systemBarsInsets.right / density;
256+
// For native insets the value gets automatically corrected when the IME is visible (in newer WebView versions),
257+
// but for these injected values we have to handle that manually (for all WebView versions).
258+
float bottomPx = (keyboardVisible ? 0 : systemBarsInsets.bottom) / density;
259+
float leftPx = systemBarsInsets.left / density;
314260

315261
// Execute JavaScript to inject the CSS
316262
getBridge().executeOnMainThread(() -> {
@@ -337,20 +283,19 @@ private void injectSafeAreaCSS(int top, int right, int bottom, int left) {
337283
}
338284

339285
private void setStyle(String style, String bar) {
340-
String requestedStyle = style;
341286
if (style.equals(STYLE_DEFAULT)) {
342287
style = getStyleForTheme();
343288
}
344289

345290
Window window = getActivity().getWindow();
346291
WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, window.getDecorView());
347292
if (bar.isEmpty() || bar.equals(BAR_STATUS_BAR)) {
348-
currentStatusBarStyle = requestedStyle;
293+
currentStatusBarStyle = style;
349294
windowInsetsControllerCompat.setAppearanceLightStatusBars(!style.equals(STYLE_DARK));
350295
}
351296

352297
if (bar.isEmpty() || bar.equals(BAR_GESTURE_BAR)) {
353-
currentGestureBarStyle = requestedStyle;
298+
currentGestureBarStyle = style;
354299
windowInsetsControllerCompat.setAppearanceLightNavigationBars(!style.equals(STYLE_DARK));
355300
}
356301

@@ -364,24 +309,20 @@ private void setHidden(boolean hide, String bar) {
364309
if (hide) {
365310
if (bar.isEmpty()) {
366311
windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars());
367-
navBarVisible = false;
368312
} else if (bar.equals(BAR_STATUS_BAR)) {
369313
windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.statusBars());
370314
} else if (bar.equals(BAR_GESTURE_BAR)) {
371315
windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.navigationBars());
372-
navBarVisible = false;
373316
}
374317
return;
375318
}
376319

377320
if (bar.isEmpty()) {
378321
windowInsetsControllerCompat.show(WindowInsetsCompat.Type.systemBars());
379-
navBarVisible = true;
380322
} else if (bar.equals(BAR_STATUS_BAR)) {
381323
windowInsetsControllerCompat.show(WindowInsetsCompat.Type.statusBars());
382324
} else if (bar.equals(BAR_GESTURE_BAR)) {
383325
windowInsetsControllerCompat.show(WindowInsetsCompat.Type.navigationBars());
384-
navBarVisible = true;
385326
}
386327
}
387328

0 commit comments

Comments
 (0)