feat!: support previous non-E2E (#1817)

* feat: support previous non-e2e (add FrameLayout wrapper)
* feat: implement internal SystemBar plugin
* feat: implement StatusBar plugin JS API (SystemBarPlugin)
* feat!: force custom statusbarView for all SDKs
* chore: various cleanup, refactors, fixes, and docs from recent changes
* feat: use getComputedStyle for setBackgroundColor
* chore: suppress deprecation warnings for method using setNavigationBarColor
* chore: return null when rootView is null
* fix: setOnApplyWindowInsetsListener to return insets
* fix: setting appearance when e2e is enabled
* fix: set statusBarColor to transparent, use new statusBar UI
This commit is contained in:
エリス
2025-09-27 02:21:17 +09:00
committed by GitHub
parent 7d7f511023
commit 76aa938002
8 changed files with 538 additions and 38 deletions

View File

@@ -39,6 +39,10 @@ module.exports = {
// Core Splash Screen
modulemapper.clobbers('cordova/plugin/android/splashscreen', 'navigator.splashscreen');
// Attach the internal statusBar utility to window.statusbar
// see the file under plugin/android/statusbar.js
modulemapper.clobbers('cordova/plugin/android/statusbar', 'window.statusbar');
var APP_PLUGIN_NAME = Number(cordova.platformVersion.split('.')[0]) >= 4 ? 'CoreAndroid' : 'App';
// Inject a listener for the backbutton on the document.

View File

@@ -0,0 +1,86 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
var exec = require('cordova/exec');
var statusBarVisible = true;
var statusBar = {};
Object.defineProperty(statusBar, 'visible', {
configurable: false,
enumerable: true,
get: function () {
if (window.StatusBar) {
// try to let the StatusBar plugin handle it
return window.StatusBar.isVisible;
}
return statusBarVisible;
},
set: function (value) {
if (window.StatusBar) {
// try to let the StatusBar plugin handle it
if (value) {
window.StatusBar.show();
} else {
window.StatusBar.hide();
}
} else {
statusBarVisible = value;
exec(null, null, 'SystemBarPlugin', 'setStatusBarVisible', [!!value]);
}
}
});
Object.defineProperty(statusBar, 'setBackgroundColor', {
configurable: false,
enumerable: false,
writable: false,
value: function (value) {
var script = document.querySelector('script[src$="cordova.js"]');
script.style.color = value;
var rgbStr = window.getComputedStyle(script).getPropertyValue('color');
if (!rgbStr.match(/^rgb/)) { return; }
var rgbVals = rgbStr.match(/\d+/g).map(function (v) { return parseInt(v, 10); });
if (rgbVals.length < 3) {
return;
} else if (rgbVals.length === 3) {
rgbVals = [255].concat(rgbVals);
}
const padRgb = (val) => val.toString(16).padStart(2, '0');
const a = padRgb(rgbVals[0]);
const r = padRgb(rgbVals[1]);
const g = padRgb(rgbVals[2]);
const b = padRgb(rgbVals[3]);
const hexStr = '#' + a + r + g + b;
if (window.StatusBar) {
window.StatusBar.backgroundColorByHexString(hexStr);
} else {
exec(null, null, 'SystemBarPlugin', 'setStatusBarBackgroundColor', rgbVals);
}
}
});
module.exports = statusBar;

View File

@@ -77,6 +77,14 @@ public class ConfigXmlParser {
)
);
pluginEntries.add(
new PluginEntry(
SystemBarPlugin.PLUGIN_NAME,
"org.apache.cordova.SystemBarPlugin",
true
)
);
pluginEntries.add(
new PluginEntry(
SplashScreenPlugin.PLUGIN_NAME,

View File

@@ -29,9 +29,10 @@ import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -42,7 +43,11 @@ import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.splashscreen.SplashScreen;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
/**
* This class is the main Android activity that represents the Cordova
@@ -100,6 +105,9 @@ public class CordovaActivity extends AppCompatActivity {
private SplashScreen splashScreen;
private boolean canEdgeToEdge = false;
private boolean isFullScreen = false;
/**
* Called when the activity is first created.
*/
@@ -113,6 +121,9 @@ public class CordovaActivity extends AppCompatActivity {
// need to activate preferences before super.onCreate to avoid "requestFeature() must be called before adding content" exception
loadConfig();
canEdgeToEdge = preferences.getBoolean("AndroidEdgeToEdge", false)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM;
String logLevel = preferences.getString("loglevel", "ERROR");
LOG.setLogLevel(logLevel);
@@ -127,7 +138,10 @@ public class CordovaActivity extends AppCompatActivity {
LOG.d(TAG, "The SetFullscreen configuration is deprecated in favor of Fullscreen, and will be removed in a future version.");
preferences.set("Fullscreen", true);
}
if (preferences.getBoolean("Fullscreen", false)) {
isFullScreen = preferences.getBoolean("Fullscreen", false);
if (isFullScreen) {
// NOTE: use the FullscreenNotImmersive configuration key to set the activity in a REAL full screen
// (as was the case in previous cordova versions)
if (!preferences.getBoolean("FullscreenNotImmersive", false)) {
@@ -184,26 +198,56 @@ public class CordovaActivity extends AppCompatActivity {
//Suppressing warnings in AndroidStudio
@SuppressWarnings({"deprecation", "ResourceType"})
protected void createViews() {
//Why are we setting a constant as the ID? This should be investigated
appView.getView().setId(100);
appView.getView().setLayoutParams(new FrameLayout.LayoutParams(
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
// Root FrameLayout
FrameLayout rootLayout = new FrameLayout(this);
rootLayout.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
ViewGroup.LayoutParams.MATCH_PARENT
));
setContentView(appView.getView());
// WebView
View webView = appView.getView();
webView.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
if (preferences.contains("BackgroundColor")) {
try {
int backgroundColor = preferences.getInteger("BackgroundColor", Color.BLACK);
// Background of activity:
appView.getView().setBackgroundColor(backgroundColor);
}
catch (NumberFormatException e){
e.printStackTrace();
}
}
// Create StatusBar view that will overlay the top inset
View statusBarView = new View(this);
statusBarView.setTag("statusBarView");
appView.getView().requestFocusFromTouch();
// Handle Window Insets
ViewCompat.setOnApplyWindowInsetsListener(rootLayout, (v, insets) -> {
Insets bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()
);
boolean isStatusBarVisible = statusBarView.getVisibility() != View.GONE;
int top = isStatusBarVisible && !canEdgeToEdge && !isFullScreen ? bars.top : 0;
int bottom = !canEdgeToEdge && !isFullScreen ? bars.bottom : 0;
FrameLayout.LayoutParams webViewParams = (FrameLayout.LayoutParams) webView.getLayoutParams();
webViewParams.setMargins(bars.left, top, bars.right, bottom);
webView.setLayoutParams(webViewParams);
FrameLayout.LayoutParams statusBarParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
top,
Gravity.TOP
);
statusBarView.setLayoutParams(statusBarParams);
return insets;
});
rootLayout.addView(webView);
rootLayout.addView(statusBarView);
setContentView(rootLayout);
rootLayout.post(() -> ViewCompat.requestApplyInsets(rootLayout));
webView.requestFocusFromTouch();
}
/**

View File

@@ -155,10 +155,13 @@ public class SplashScreenPlugin extends CordovaPlugin {
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
splashScreenViewProvider.remove();
webView.getPluginManager().postMessage("updateSystemBars", null);
}
}).start();
}
});
} else {
webView.getPluginManager().postMessage("updateSystemBars", null);
}
}

View File

@@ -0,0 +1,374 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.ViewParent;
import android.view.Window;
import android.view.WindowInsetsController;
import android.widget.FrameLayout;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import org.json.JSONArray;
import org.json.JSONException;
public class SystemBarPlugin extends CordovaPlugin {
static final String PLUGIN_NAME = "SystemBarPlugin";
static final int INVALID_COLOR = -1;
// Internal variables
private Context context;
private Resources resources;
private int overrideStatusBarBackgroundColor = INVALID_COLOR;
private boolean canEdgeToEdge = false;
@Override
protected void pluginInitialize() {
context = cordova.getContext();
resources = context.getResources();
canEdgeToEdge = preferences.getBoolean("AndroidEdgeToEdge", false)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
cordova.getActivity().runOnUiThread(this::updateSystemBars);
}
@Override
public void onResume(boolean multitasking) {
super.onResume(multitasking);
cordova.getActivity().runOnUiThread(this::updateSystemBars);
}
@Override
public Object onMessage(String id, Object data) {
if (id.equals("updateSystemBars")) {
cordova.getActivity().runOnUiThread(this::updateSystemBars);
}
return null;
}
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if(canEdgeToEdge) {
return false;
}
if ("setStatusBarVisible".equals(action)) {
boolean visible = args.getBoolean(0);
cordova.getActivity().runOnUiThread(() -> setStatusBarVisible(visible));
} else if ("setStatusBarBackgroundColor".equals(action)) {
cordova.getActivity().runOnUiThread(() -> setStatusBarBackgroundColor(args));
} else {
return false;
}
callbackContext.success();
return true;
}
/**
* Allow the app to override the status bar visibility from JS API.
* If for some reason the statusBarView could not be discovered, it will silently ignore
* the change request
*
* @param visible should the status bar be visible?
*/
private void setStatusBarVisible(final boolean visible) {
View statusBar = getStatusBarView(webView);
if (statusBar != null) {
statusBar.setVisibility(visible ? View.VISIBLE : View.GONE);
FrameLayout rootLayout = getRootLayout(webView);
if (rootLayout != null) {
ViewCompat.requestApplyInsets(rootLayout);
}
}
}
/**
* Allow the app to override the status bar background color from JS API.
* If the supplied ARGB is invalid or fails to parse, it will silently ignore
* the change request.
*
* @param argbVals {A, R, G, B}
*/
private void setStatusBarBackgroundColor(JSONArray argbVals) {
try {
int a = argbVals.getInt(0);
int r = argbVals.getInt(1);
int g = argbVals.getInt(2);
int b = argbVals.getInt(3);
String hexColor = String.format("#%02X%02X%02X%02X", a, r, g, b);
int parsedColor = parseColorFromString(hexColor);
if (parsedColor == INVALID_COLOR) return;
overrideStatusBarBackgroundColor = parsedColor;
updateStatusBar(overrideStatusBarBackgroundColor);
} catch (JSONException e) {
// Silently skip
}
}
/**
* Attempt to update all system bars (status, navigation and gesture bars) in various points
* of the apps life cycle.
* For example:
* 1. Device configurations between (E.g. between dark and light mode)
* 2. User resumes the app
* 3. App transitions from SplashScreen Theme to App's Theme
*/
private void updateSystemBars() {
// Update Root View Background Color
int rootViewBackgroundColor = getPreferenceBackgroundColor();
if (rootViewBackgroundColor == INVALID_COLOR) {
rootViewBackgroundColor = canEdgeToEdge ? Color.TRANSPARENT : getUiModeColor();
}
updateRootView(rootViewBackgroundColor);
// Update StatusBar Background Color
int statusBarBackgroundColor;
if (overrideStatusBarBackgroundColor != INVALID_COLOR) {
statusBarBackgroundColor = overrideStatusBarBackgroundColor;
} else if (preferences.contains("StatusBarBackgroundColor")) {
statusBarBackgroundColor = getPreferenceStatusBarBackgroundColor();
} else if(preferences.contains("BackgroundColor")){
statusBarBackgroundColor = rootViewBackgroundColor;
} else {
statusBarBackgroundColor = canEdgeToEdge ? Color.TRANSPARENT : getUiModeColor();
}
updateStatusBar(statusBarBackgroundColor);
}
/**
* Updates the root layout's background color with the supplied color int.
* It will also determine if the background color is light or dark to properly adjust the
* appearance of the navigation/gesture bar's icons so it will not clash with the background.
* <p>
* System bars (navigation & gesture) on SDK 25 or lower is forced to black as the appearance
* of the fonts can not be updated.
* System bars (navigation & gesture) on SDK 26 or greater allows custom background color.
* <p/>
*
* @param bgColor Background color
*/
@SuppressWarnings("deprecation")
private void updateRootView(int bgColor) {
Window window = cordova.getActivity().getWindow();
// Set the root view's background color. Works on SDK 36+
View root = cordova.getActivity().findViewById(android.R.id.content);
if (root != null) root.setBackgroundColor(bgColor);
// Automatically set the font and icon color of the system bars based on background color.
boolean isBackgroundColorLight;
if(bgColor == Color.TRANSPARENT) {
isBackgroundColorLight = isColorLight(getUiModeColor());
} else {
isBackgroundColorLight = isColorLight(bgColor);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowInsetsController controller = window.getInsetsController();
if (controller != null) {
int appearance = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
if (isBackgroundColorLight) {
controller.setSystemBarsAppearance(0, appearance);
} else {
controller.setSystemBarsAppearance(appearance, appearance);
}
}
}
WindowInsetsControllerCompat controllerCompat = WindowCompat.getInsetsController(window, window.getDecorView());
controllerCompat.setAppearanceLightNavigationBars(isBackgroundColorLight);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window.setNavigationBarColor(bgColor);
} else {
window.setNavigationBarColor(Color.BLACK);
}
}
/**
* Updates the statusBarView background color with the supplied color int.
* It will also determine if the background color is light or dark to properly adjust the
* appearance of the status bar so the font will not clash with the background.
*
* @param bgColor Background color
*/
private void updateStatusBar(int bgColor) {
Window window = cordova.getActivity().getWindow();
View statusBar = getStatusBarView(webView);
if (statusBar != null) {
statusBar.setBackgroundColor(bgColor);
}
// Automatically set the font and icon color of the system bars based on background color.
boolean isStatusBarBackgroundColorLight;
if(bgColor == Color.TRANSPARENT) {
isStatusBarBackgroundColorLight = isColorLight(getUiModeColor());
} else {
isStatusBarBackgroundColorLight = isColorLight(bgColor);
}
WindowInsetsControllerCompat controllerCompat = WindowCompat.getInsetsController(window, window.getDecorView());
controllerCompat.setAppearanceLightStatusBars(isStatusBarBackgroundColorLight);
}
/**
* Determines if the supplied color's appearance is light.
*
* @param color color
* @return boolean value true is returned when the color is light.
*/
private static boolean isColorLight(int color) {
double r = Color.red(color) / 255.0;
double g = Color.green(color) / 255.0;
double b = Color.blue(color) / 255.0;
double luminance = 0.299 * r + 0.587 * g + 0.114 * b;
return luminance > 0.5;
}
/**
* Returns the StatusBarBackgroundColor preference value.
* If the value is missing or fails to parse, it will attempt to try to guess the background
* color by extracting from the apps R.color.cdv_background_color or determine from the uiModes.
* If all fails, the color normally used in light mode is returned.
*
* @return int
*/
private int getPreferenceStatusBarBackgroundColor() {
String colorString = preferences.getString("StatusBarBackgroundColor", null);
int parsedColor = parseColorFromString(colorString);
if (parsedColor != INVALID_COLOR) return parsedColor;
return getUiModeColor(); // fallback
}
/**
* Returns the BackgroundColor preference value.
* If missing or fails to decode, it will return INVALID_COLOR (-1).
*
* @return int
*/
private int getPreferenceBackgroundColor() {
try {
return preferences.getInteger("BackgroundColor", INVALID_COLOR);
} catch (NumberFormatException e) {
LOG.e(PLUGIN_NAME, "Invalid background color argument. Example valid string: '0x00000000'");
return INVALID_COLOR;
}
}
/**
* Tries to find and return the rootLayout.
*
* @param webView CordovaWebView
* @return FrameLayout|null
*/
private FrameLayout getRootLayout(CordovaWebView webView) {
ViewParent parent = webView.getView().getParent();
if (parent instanceof FrameLayout) {
return (FrameLayout) parent;
}
return null;
}
/**
* Tries to find and return the statusBarView.
*
* @param webView CordovaWebView
* @return View|null
*/
private View getStatusBarView(CordovaWebView webView) {
FrameLayout rootView = getRootLayout(webView);
if (rootView == null) {
return null;
}
for (int i = 0; i < rootView.getChildCount(); i++) {
View child = rootView.getChildAt(i);
Object tag = child.getTag();
if ("statusBarView".equals(tag)) {
return child;
}
}
return null;
}
/**
* Determines the background color for status bar & root layer.
* The color will come from the app's R.color.cdv_background_color.
* If for some reason the resource is missing, it will try to fallback on the uiMode.
* <p>
* The uiMode as follows.
* If night mode: "#121318" (android.R.color.system_background_dark)
* If day mode: "#FAF8FF" (android.R.color.system_background_light)
* If all fails, light mode will be returned.
* </p>
* The hex values are supplied instead of "android.R.color" for backwards compatibility.
*
* @return int color
*/
@SuppressLint("DiscouragedApi")
private int getUiModeColor() {
boolean isNightMode = (resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
String fallbackColor = isNightMode ? "#121318" : "#FAF8FF";
int colorResId = resources.getIdentifier("cdv_background_color", "color", context.getPackageName());
return colorResId != 0
? ContextCompat.getColor(context, colorResId)
: Color.parseColor(fallbackColor);
}
/**
* Parse color string that would be provided by app developers.
* If the color string is empty or unable to parse, it will return INVALID_COLOR (-1).
*
* @param colorPref hex string value, #AARRGGBB or #RRGGBB
* @return int
*/
private int parseColorFromString(final String colorPref) {
if (colorPref.isEmpty()) return INVALID_COLOR;
try {
return Color.parseColor(colorPref);
} catch (IllegalArgumentException ignore) {
LOG.e(PLUGIN_NAME, "Invalid color hex code. Valid format: #RRGGBB or #AARRGGBB");
return INVALID_COLOR;
}
}
}

View File

@@ -382,26 +382,6 @@ function updateProjectTheme (platformConfig, locations) {
const themes = xmlHelpers.parseElementtreeSync(locations.themes);
const splashScreenTheme = themes.find('style[@name="Theme.App.SplashScreen"]');
// Update edge-to-edge settings in app theme.
let hasE2E = false; // default case
const preferenceE2E = platformConfig.getPreference('AndroidEdgeToEdge', this.platform);
if (!preferenceE2E) {
events.emit('verbose', 'The preference name "AndroidEdgeToEdge" was not set. Defaulting to "false".');
} else {
const hasInvalidPreferenceE2E = preferenceE2E !== 'true' && preferenceE2E !== 'false';
if (hasInvalidPreferenceE2E) {
events.emit('verbose', 'Preference name "AndroidEdgeToEdge" has an invalid value. Valid values are "true" or "false". Defaulting to "false"');
}
hasE2E = hasInvalidPreferenceE2E ? false : preferenceE2E === 'true';
}
const optOutE2EKey = 'android:windowOptOutEdgeToEdgeEnforcement';
const optOutE2EItem = splashScreenTheme.find(`item[@name="${optOutE2EKey}"]`);
const optOutE2EValue = !hasE2E ? 'true' : 'false';
optOutE2EItem.text = optOutE2EValue;
events.emit('verbose', `Updating theme item "${optOutE2EKey}" with value "${optOutE2EValue}"`);
let splashBg = platformConfig.getPreference('AndroidWindowSplashScreenBackground', this.platform);
if (!splashBg) {
splashBg = platformConfig.getPreference('SplashScreenBackgroundColor', this.platform);

View File

@@ -37,5 +37,6 @@
<style name="Theme.Cordova.App.DayNight" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:colorBackground">@color/cdv_background_color</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>