Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class CapConfig {
private String errorPath;
private boolean zoomableWebView = false;
private boolean resolveServiceWorkerRequests = true;
private String adjustMarginsForEdgeToEdge = "auto";

// Embedded
private String startPath;
Expand Down Expand Up @@ -286,6 +287,7 @@ private void deserializeConfig(@Nullable Context context) {
webContentsDebuggingEnabled = JSONUtils.getBoolean(configJSON, "android.webContentsDebuggingEnabled", isDebug);
zoomableWebView = JSONUtils.getBoolean(configJSON, "android.zoomEnabled", JSONUtils.getBoolean(configJSON, "zoomEnabled", false));
resolveServiceWorkerRequests = JSONUtils.getBoolean(configJSON, "android.resolveServiceWorkerRequests", true);
adjustMarginsForEdgeToEdge = JSONUtils.getString(configJSON, "android.adjustMarginsForEdgeToEdge", "auto");

String logBehavior = JSONUtils.getString(
configJSON,
Expand Down Expand Up @@ -582,6 +584,7 @@ public static class Builder {
private int minHuaweiWebViewVersion = DEFAULT_HUAWEI_WEBVIEW_VERSION;
private boolean zoomableWebView = false;
private boolean resolveServiceWorkerRequests = true;
private String adjustMarginsForEdgeToEdge = "auto";

// Embedded
private String startPath = null;
Expand Down
189 changes: 27 additions & 162 deletions android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
package com.getcapacitor.plugin;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.util.TypedValue;
import android.view.View;
import android.view.Window;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
Expand All @@ -19,9 +13,10 @@
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.WebViewListener;
import com.getcapacitor.annotation.CapacitorPlugin;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@CapacitorPlugin
public class SystemBars extends Plugin {
Expand All @@ -32,14 +27,6 @@
static final String BAR_STATUS_BAR = "StatusBar";
static final String BAR_GESTURE_BAR = "NavigationBar";

static final String INSETS_HANDLING_CSS = "css";
static final String INSETS_HANDLING_DISABLE = "disable";

// https://issues.chromium.org/issues/40699457
private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140;
// https://issues.chromium.org/issues/457682720
private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_KEYBOARD_FIX = 144;

static final String viewportMetaJSFunction = """
function capacitorSystemBarsCheckMetaViewport() {
const meta = document.querySelectorAll("meta[name=viewport]");
Expand All @@ -50,57 +37,42 @@
const metaContent = meta[meta.length - 1].content;
return metaContent.includes("viewport-fit=cover");
}

capacitorSystemBarsCheckMetaViewport();
""";

private boolean insetHandlingEnabled = true;
private boolean hasViewportCover = false;

private String currentStatusBarStyle = STYLE_DEFAULT;
private String currentGestureBarStyle = STYLE_DEFAULT;

@Override
public void load() {
getBridge().getWebView().addJavascriptInterface(this, "CapacitorSystemBarsAndroidInterface");
super.load();

initSystemBars();
}

@Override
protected void handleOnStart() {
super.handleOnStart();

this.getBridge().addWebViewListener(
new WebViewListener() {
@Override
public void onPageCommitVisible(WebView view, String url) {
super.onPageCommitVisible(view, url);
getBridge().getWebView().requestApplyInsets();
}
}
);
}
private boolean hasFixedWebView() {
PackageInfo packageInfo = WebViewCompat.getCurrentWebViewPackage(bridge.getContext());
Pattern pattern = Pattern.compile("(\\d+)");
Matcher matcher = pattern.matcher(packageInfo.versionName);

@Override
protected void handleOnConfigurationChanged(Configuration newConfig) {
super.handleOnConfigurationChanged(newConfig);
if (!matcher.find()) {
return false;
}

String majorVersionStr = matcher.group(0);
int majorVersion = Integer.parseInt(majorVersionStr);

setStyle(currentGestureBarStyle, BAR_GESTURE_BAR);
setStyle(currentStatusBarStyle, BAR_STATUS_BAR);
return majorVersion >= 140;
}

private void initSystemBars() {
String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase(Locale.US);
boolean hidden = getConfig().getBoolean("hidden", false);
boolean disableCSSInsets = getConfig().getBoolean("disableInsets", false);

String insetsHandling = getConfig().getString("insetsHandling", "css");
if (insetsHandling.equals(INSETS_HANDLING_DISABLE)) {
insetHandlingEnabled = false;
}

initWindowInsetsListener();
initSafeAreaCSSVariables();
this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
boolean hasMetaViewportCover = res.equals("true");
if (!disableCSSInsets) {
setupSafeAreaInsets(this.hasFixedWebView(), hasMetaViewportCover);
}
});

getBridge().executeOnMainThread(() -> {
setStyle(style, "");
Expand Down Expand Up @@ -144,89 +116,16 @@
call.resolve();
}

@JavascriptInterface
public void onDOMReady() {
getActivity().runOnUiThread(() -> {
this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
hasViewportCover = res.equals("true");

getBridge().getWebView().requestApplyInsets();
});
});
}

private Insets calcSafeAreaInsets(WindowInsetsCompat insets) {
Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
if (insets.isVisible(WindowInsetsCompat.Type.ime())) {
return Insets.of(safeArea.left, safeArea.top, safeArea.right, 0);
}
return Insets.of(safeArea.left, safeArea.top, safeArea.right, safeArea.bottom);
}

private void initSafeAreaCSSVariables() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && insetHandlingEnabled) {
View v = (View) this.getBridge().getWebView().getParent();
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(v);
if (insets != null) {
Insets safeAreaInsets = calcSafeAreaInsets(insets);
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
}
}
}

private void initWindowInsetsListener() {
private void setupSafeAreaInsets(boolean hasFixedWebView, boolean hasMetaViewportCover) {
ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> {
boolean shouldPassthroughInsets = getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover;

Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());

if (shouldPassthroughInsets) {
// We need to correct for a possible shown IME
v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && hasViewportCover && insetHandlingEnabled) {
Insets safeAreaInsets = calcSafeAreaInsets(insets);
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
}

return new WindowInsetsCompat.Builder(insets)
.setInsets(
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(),
Insets.of(
systemBarsInsets.left,
systemBarsInsets.top,
systemBarsInsets.right,
getBottomInset(systemBarsInsets, keyboardVisible)
)
)
.build();
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
// We need to correct for a possible shown IME
v.setPadding(
systemBarsInsets.left,
systemBarsInsets.top,
systemBarsInsets.right,
keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom
);
if (hasFixedWebView && hasMetaViewportCover) {
return insets;
}

// Returning `WindowInsetsCompat.CONSUMED` breaks recalculation of safe area insets
// So we have to explicitly set insets to `0`
// See: https://issues.chromium.org/issues/461332423
WindowInsetsCompat newInsets = new WindowInsetsCompat.Builder(insets)
.setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0))
.build();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && hasViewportCover && insetHandlingEnabled) {
Insets safeAreaInsets = calcSafeAreaInsets(newInsets);
injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
}
Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
injectSafeAreaCSS(safeArea.top, safeArea.right, safeArea.bottom, safeArea.left);

return newInsets;
return WindowInsetsCompat.CONSUMED;
});
}

Expand Down Expand Up @@ -270,16 +169,14 @@
Window window = getActivity().getWindow();
WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, window.getDecorView());
if (bar.isEmpty() || bar.equals(BAR_STATUS_BAR)) {
currentStatusBarStyle = style;

Check failure on line 172 in android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java

View workflow job for this annotation

GitHub Actions / test-android

cannot find symbol
windowInsetsControllerCompat.setAppearanceLightStatusBars(!style.equals(STYLE_DARK));
}

if (bar.isEmpty() || bar.equals(BAR_GESTURE_BAR)) {
currentGestureBarStyle = style;

Check failure on line 177 in android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java

View workflow job for this annotation

GitHub Actions / test-android

cannot find symbol
windowInsetsControllerCompat.setAppearanceLightNavigationBars(!style.equals(STYLE_DARK));
}

getActivity().getWindow().getDecorView().setBackgroundColor(getThemeColor(getContext(), android.R.attr.windowBackground));
}

private void setHidden(boolean hide, String bar) {
Expand Down Expand Up @@ -311,36 +208,4 @@
}
return STYLE_DARK;
}

public int getThemeColor(Context context, int attrRes) {
TypedValue typedValue = new TypedValue();

Resources.Theme theme = context.getTheme();
theme.resolveAttribute(attrRes, typedValue, true);
return typedValue.data;
}

private Integer getWebViewMajorVersion() {
PackageInfo info = WebViewCompat.getCurrentWebViewPackage(getContext());
if (info != null && info.versionName != null) {
String[] versionSegments = info.versionName.split("\\.");
return Integer.valueOf(versionSegments[0]);
}

return 0;
}

private int getBottomInset(Insets systemBarsInsets, boolean keyboardVisible) {
if (getWebViewMajorVersion() < WEBVIEW_VERSION_WITH_SAFE_AREA_KEYBOARD_FIX) {
// This is a workaround for webview versions that have a bug
// that causes the bottom inset to be incorrect if the IME is visible
// See: https://issues.chromium.org/issues/457682720

if (keyboardVisible) {
return 0;
}
}

return systemBarsInsets.bottom;
}
}
12 changes: 3 additions & 9 deletions cli/src/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,17 +761,11 @@ export interface PluginsConfig {
*/
SystemBars?: {
/**
* Specifies how to handle problematic insets on Android.
* Disables the injection of device css insets into the web view.
*
* This option is only supported on Android.
*
* `css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
*
* `disable` = Disable CSS variables injection.
*
* @default "css"
* @default false
*/
insetsHandling?: 'css' | 'disable';
disableInsets?: boolean;
/**
* The style of the text and icons of the system bars.
*
Expand Down
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"build": "npm run clean && npm run docgen && npm run transpile && npm run rollup",
"build:nativebridge": "tsc native-bridge.ts --target es2017 --moduleResolution node --outDir build && rollup --config rollup.bridge.config.js",
"clean": "rimraf dist",
"docgen": "docgen --api CapacitorCookiesPlugin --output-readme cookies.md && docgen --api CapacitorHttpPlugin --output-readme http.md && docgen --api SystemBarsPlugin --output-readme system-bars.md",
"docgen": "docgen --api CapacitorCookiesPlugin --output-readme cookies.md && docgen --api CapacitorHttpPlugin --output-readme http.md && docgen --api SystemBarsPlugin --output-readme systembars.md",
"prepublishOnly": "npm run build",
"rollup": "rollup --config rollup.config.js",
"transpile": "tsc",
Expand Down
4 changes: 2 additions & 2 deletions core/src/core-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,14 +617,14 @@ export interface SystemBarsPlugin {
*
* @since 8.0.0
*/
show(options?: SystemBarsVisibilityOptions): Promise<void>;
show(options: SystemBarsVisibilityOptions): Promise<void>;

/**
* Hide the system bars.
*
* @since 8.0.0
*/
hide(options?: SystemBarsVisibilityOptions): Promise<void>;
hide(options: SystemBarsVisibilityOptions): Promise<void>;

/**
* Set the animation to use when showing / hiding the status bar.
Expand Down
Loading
Loading