Skip to content
Draft
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 @@ -655,6 +655,7 @@ private void registerAllPlugins() {
this.registerPlugin(com.getcapacitor.plugin.CapacitorCookies.class);
this.registerPlugin(com.getcapacitor.plugin.WebView.class);
this.registerPlugin(com.getcapacitor.plugin.CapacitorHttp.class);
this.registerPlugin(com.getcapacitor.plugin.SystemBars.class);

for (Class<? extends Plugin> pluginClass : this.initialPlugins) {
this.registerPlugin(pluginClass);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package com.getcapacitor.plugin;

import android.content.res.Configuration;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.Window;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.getcapacitor.Logger;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import java.util.Locale;

@CapacitorPlugin
public class SystemBars extends Plugin {

static final String STYLE_LIGHT = "LIGHT";
static final String STYLE_DARK = "DARK";
static final String STYLE_DEFAULT = "DEFAULT";
static final String INSET_TOP = "top";
static final String INSET_BOTTOM = "bottom";

@Override
public void load() {
super.load();
initSystemBars();
}

private void initSystemBars() {
boolean enableInsets = getConfig().getBoolean("enableInsets", true);
String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase();

if (enableInsets) {
setupSafeAreaInsets();
}

getBridge()
.executeOnMainThread(() -> {
setStyle(style, "");
initOverlay();
});
}

private void initOverlay() {
if (Build.VERSION.SDK_INT < 35) {
Window window = getActivity().getWindow();
View decorView = window.getDecorView();

int uiOptions = decorView.getSystemUiVisibility();
int color;

uiOptions = uiOptions | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
color = Color.TRANSPARENT;

window.getDecorView().setSystemUiVisibility(uiOptions);
window.setStatusBarColor(color);
window.setNavigationBarColor(color);
}
}

@PluginMethod
public void setStyle(final PluginCall call) {
String inset = call.getString("inset", "").toLowerCase(Locale.US);
String style = call.getString("style", STYLE_DEFAULT);

getBridge()
.executeOnMainThread(() -> {
setStyle(style, inset);
call.resolve();
});
}

@PluginMethod
public void setHidden(final PluginCall call) {
boolean hidden = call.getBoolean("hidden", false);
String inset = call.getString("inset", "").toLowerCase(Locale.US);

getBridge()
.executeOnMainThread(() -> {
setHidden(hidden, inset);
call.resolve();
});
}

private void setupSafeAreaInsets() {
View decorView = getActivity().getWindow().getDecorView();

ViewCompat.setOnApplyWindowInsetsListener(decorView, (v, insets) -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
// Android 15+ supports edge to edge
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
Insets displayCutout = insets.getInsets(WindowInsetsCompat.Type.displayCutout());

int top = Math.max(systemBars.top, displayCutout.top);
int bottom = Math.max(systemBars.bottom, displayCutout.bottom);
int left = Math.max(systemBars.left, displayCutout.left);
int right = Math.max(systemBars.right, displayCutout.right);

injectSafeAreaCSS(top, right, bottom, left);
} else {
Insets statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars());
Insets navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars());

injectSafeAreaCSS(statusBarInsets.top, statusBarInsets.right, navBarInsets.bottom, statusBarInsets.left);
}

return WindowInsetsCompat.CONSUMED;
});
}

private void injectSafeAreaCSS(int top, int right, int bottom, int left) {
// Convert pixels to density-independent pixels
float density = getActivity().getResources().getDisplayMetrics().density;
float topPx = top / density;
float rightPx = right / density;
float bottomPx = bottom / density;
float leftPx = left / density;

// Execute JavaScript to inject the CSS
getBridge()
.executeOnMainThread(() -> {
if (bridge != null && bridge.getWebView() != null) {
String script = String.format(
Locale.US,
"""
try {
document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx");
document.documentElement.style.setProperty("--safe-area-inset-right", "%dpx");
document.documentElement.style.setProperty("--safe-area-inset-bottom", "%dpx");
document.documentElement.style.setProperty("--safe-area-inset-left", "%dpx");
window.dispatchEvent(new CustomEvent('safeAreaChanged'));
} catch(e) { console.error('Error injecting safe area CSS:', e); }
""",
(int) topPx,
(int) rightPx,
(int) bottomPx,
(int) leftPx
);

bridge.getWebView().evaluateJavascript(script, null);

Logger.info("Safe area insets injected (edge-to-edge: enabled)");
}
});
}

private void setStyle(String style, String inset) {
if (style.equals(STYLE_DEFAULT)) {
style = getStyleForTheme();
}

Window window = getActivity().getWindow();
WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, window.getDecorView());
if (inset.isEmpty() || inset.equals(INSET_TOP)) {
windowInsetsControllerCompat.setAppearanceLightStatusBars(!style.equals(STYLE_DARK));
}

if (inset.isEmpty() || inset.equals(INSET_BOTTOM)) {
windowInsetsControllerCompat.setAppearanceLightNavigationBars(!style.equals(STYLE_DARK));
}
}

private void setHidden(boolean hide, String inset) {
Window window = getActivity().getWindow();
WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, window.getDecorView());

if (hide) {
if (inset.isEmpty() || inset.equals(INSET_TOP)) {
windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.statusBars());
}
if (inset.isEmpty() || inset.equals(INSET_BOTTOM)) {
windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.navigationBars());
}
return;
}

if (inset.isEmpty() || inset.equals(INSET_TOP)) {
windowInsetsControllerCompat.show(WindowInsetsCompat.Type.systemBars());
}
if (inset.isEmpty() || inset.equals(INSET_BOTTOM)) {
windowInsetsControllerCompat.show(WindowInsetsCompat.Type.navigationBars());
}
}

private String getStyleForTheme() {
int currentNightMode = getActivity().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (currentNightMode != Configuration.UI_MODE_NIGHT_YES) {
return STYLE_LIGHT;
}
return STYLE_DARK;
}
}
21 changes: 21 additions & 0 deletions cli/src/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,4 +710,25 @@ export interface PluginsConfig {
*/
enabled?: boolean;
};

/**
* System Bars plugin configuration
*
* @since 8.0.0
*/
SystemBars?: {
/**
* Enables the injection of device css insets into the webview.
*
* Only available on Android.
*
* @default true
*/
enableInsets?: boolean;

/**
* Style of the text of the status bar.
*/
style?: string;
};
}
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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": "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
108 changes: 108 additions & 0 deletions core/src/core-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,3 +494,111 @@ export const CapacitorHttp = registerPlugin<CapacitorHttpPlugin>('CapacitorHttp'
});

/******** END HTTP PLUGIN ********/

/******** SYSTEM BARS PLUGIN ********/

/**
* Available status bar styles.
*/
export enum SystemBarStyle {
/**
* Light system bar content on a dark background.
*
* @since 8.0.0
*/
Dark = 'DARK',

/**
* For dark system bar content on a light background.
*
* @since 8.0.0
*/
Light = 'LIGHT',

/**
* The style is based on the device appearance or the underlying content.
* If the device is using Dark mode, the statusbar text will be light.
* If the device is using Light mode, the statusbar text will be dark.
*
* @since 8.0.0
*/
Default = 'DEFAULT',
}

/**
* Available inset edges.
*/
export type SystemBarInsets = 'top' | 'bottom' | 'left' | 'right';

export interface SystemBarsStyleOptions {
/**
* Style of the text of the status bar.
*
* @since 8.0.0
* @default default
* @example "DARK"
*/
style: SystemBarStyle;

/**
* The inset edge for which to apply the style.
*
* @since 8.0.0
* @default null
* @example "top"
*/
inset?: Omit<SystemBarInsets, 'left, right'>;
}

export interface SystemBarsHiddenOptions {
hidden: boolean;
/**
* The inset edge for which to hide.
*
* @since 8.0.0
* @default null
* @example "top"
*/
inset?: SystemBarInsets;
}

export interface SystemBarsOverlayOptions {
enabled: boolean;
}

export interface SystemBarsPlugin {
/**
* Set the current style of the status bar.
*
* @since 8.0.0
*/
setStyle(options: SystemBarsStyleOptions): Promise<void>;
/**
* Set the visibility of the status bar.
*
* @since 8.0.0
*/
setHidden(options: SystemBarsHiddenOptions): Promise<void>;

setOverlay(options: SystemBarsOverlayOptions): Promise<void>;
}

export class SystemBarsPluginWeb extends WebPlugin implements SystemBarsPlugin {
async setStyle(): Promise<void> {
this.unavailable('not available for web');
}

async setHidden(): Promise<void> {
this.unavailable('not available for web');
}

async setOverlay(): Promise<void> {
this.unavailable('not available for web');
}
}

export const SystemBars = registerPlugin<SystemBarsPlugin>('SystemBars', {
web: () => new SystemBarsPluginWeb(),
});

/******** END SYSTEM BARS PLUGIN ********/
12 changes: 11 additions & 1 deletion core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ export { Capacitor, registerPlugin } from './global';
export { WebPlugin, ListenerCallback } from './web-plugin';

// Core Plugins APIs
export { CapacitorCookies, CapacitorHttp, WebView, buildRequestInit } from './core-plugins';
export {
SystemBars,
SystemBarInsets,
SystemBarStyle,
CapacitorCookies,
CapacitorHttp,
WebView,
buildRequestInit,
} from './core-plugins';

// Core Plugin definitions
export type {
Expand All @@ -31,6 +39,8 @@ export type {
HttpResponseType,
WebViewPath,
WebViewPlugin,
SystemBarsHiddenOptions,
SystemBarsStyleOptions,
} from './core-plugins';

// Constants
Expand Down
Loading
Loading