diff --git a/build-extras.gradle b/build-extras.gradle
index 6b606e0ed..d20a92f78 100644
--- a/build-extras.gradle
+++ b/build-extras.gradle
@@ -2,6 +2,15 @@ android {
packagingOptions {
pickFirst 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
}
+
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
+ rootProject.file('../../proguard-rules.pro')
+ }
+ }
}
configurations {
@@ -12,4 +21,4 @@ configurations {
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk18on'
exclude group: 'org.bouncycastle', module: 'bcprov-jdk18on'
}
-}
\ No newline at end of file
+}
diff --git a/proguard-rules.pro b/proguard-rules.pro
new file mode 100644
index 000000000..780f75882
--- /dev/null
+++ b/proguard-rules.pro
@@ -0,0 +1,22 @@
+# Keep Cordova's reflection-loaded bridge and Acode's native plugin code stable.
+-keep class org.apache.cordova.** { *; }
+-keep class com.foxdebug.** { *; }
+-keep class com.silkimen.** { *; }
+-keep class com.verso.** { *; }
+-keep class admob.plus.** { *; }
+
+# SSH and crypto providers perform runtime discovery and reflective registration.
+-keep class com.sshtools.** { *; }
+-keep class org.bouncycastle.** { *; }
+
+# Maverick's optional desktop thread-dump diagnostic references JMX. Android
+# does not ship these classes, and Acode never enables maverick.threadDump.
+-dontwarn java.lang.management.ManagementFactory
+-dontwarn java.lang.management.ThreadInfo
+-dontwarn java.lang.management.ThreadMXBean
+
+# cordova-plugin-buildinfo loads the active variant's BuildConfig by name.
+-keep class **.BuildConfig { *; }
+
+-keepattributes RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,AnnotationDefault
+-keepattributes Signature,Exceptions,InnerClasses,EnclosingMethod
diff --git a/res/android/values/themes.xml b/res/android/values/themes.xml
index 62745020d..4a96e96e0 100644
--- a/res/android/values/themes.xml
+++ b/res/android/values/themes.xml
@@ -5,6 +5,5 @@
- @color/teardrop
- @color/teardrop
- @color/teardrop
- - true
diff --git a/src/plugins/browser/android/com/foxdebug/browser/BrowserActivity.java b/src/plugins/browser/android/com/foxdebug/browser/BrowserActivity.java
index abd80316e..b41a2a125 100644
--- a/src/plugins/browser/android/com/foxdebug/browser/BrowserActivity.java
+++ b/src/plugins/browser/android/com/foxdebug/browser/BrowserActivity.java
@@ -2,7 +2,6 @@
import android.app.Activity;
import android.content.Intent;
-import android.graphics.Color;
import android.graphics.Insets;
import android.os.Build;
import android.os.Bundle;
@@ -75,15 +74,17 @@ private void setSystemTheme(int systemBarColor) {
try {
// Using reflection makes sure any 5.0+ device will work without having to compile with SDK level 21
- window
- .getClass()
- .getMethod("setNavigationBarColor", int.class)
- .invoke(window, systemBarColor);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ window
+ .getClass()
+ .getMethod("setNavigationBarColor", int.class)
+ .invoke(window, systemBarColor);
- window
- .getClass()
- .getMethod("setStatusBarColor", int.class)
- .invoke(window, systemBarColor);
+ window
+ .getClass()
+ .getMethod("setStatusBarColor", int.class)
+ .invoke(window, systemBarColor);
+ }
if (Build.VERSION.SDK_INT < 30) {
setStatusBarStyle(window);
@@ -95,10 +96,12 @@ private void setSystemTheme(int systemBarColor) {
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
- if (themeType.equals("light")) {
- controller.setSystemBarsAppearance(appearance, appearance);
- } else {
- controller.setSystemBarsAppearance(0, appearance);
+ if (controller != null) {
+ if (themeType.equals("light")) {
+ controller.setSystemBarsAppearance(appearance, appearance);
+ } else {
+ controller.setSystemBarsAppearance(0, appearance);
+ }
}
}
} catch (IllegalArgumentException error) {
diff --git a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java
index e9cd989e9..d2227e303 100644
--- a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java
+++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java
@@ -8,6 +8,7 @@
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.graphics.Color;
+import android.graphics.Insets;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
@@ -18,6 +19,8 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
@@ -57,6 +60,7 @@ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loadThemeColors();
+ configureEdgeToEdge();
applySystemBarColors();
Intent intent = getIntent();
@@ -103,6 +107,7 @@ protected void onCreate(Bundle savedInstanceState) {
ViewGroup.LayoutParams.MATCH_PARENT));
mainScrollView.setBackgroundColor(colorPrimaryBg);
mainScrollView.setFillViewport(true);
+ applySystemBarInsets(mainScrollView);
LinearLayout rootLayout = new LinearLayout(this);
rootLayout.setOrientation(LinearLayout.VERTICAL);
@@ -261,6 +266,7 @@ public void onClick(View v) {
rootLayout.addView(buttonsLayout);
mainScrollView.addView(rootLayout);
setContentView(mainScrollView);
+ mainScrollView.requestApplyInsets();
}
private void loadThemeColors() {
@@ -358,21 +364,73 @@ private int deriveMetaLabelColor(int secondaryText) {
Color.blue(secondaryText));
}
+ private void configureEdgeToEdge() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ getWindow().setDecorFitsSystemWindows(false);
+ }
+ }
+
+ private void applySystemBarInsets(final View view) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return;
+
+ view.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
+ @Override
+ public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ Insets safeInsets = insets.getInsets(
+ WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
+ v.setPadding(
+ safeInsets.left,
+ safeInsets.top,
+ safeInsets.right,
+ safeInsets.bottom);
+ return insets;
+ }
+ });
+ }
+
private void applySystemBarColors() {
try {
Window window = getWindow();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- window.setStatusBarColor(colorPrimaryBg);
- window.setNavigationBarColor(colorPrimaryBg);
+ View decorView = window.getDecorView();
+ decorView.setBackgroundColor(colorPrimaryBg);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ applyLegacySystemBarColor(window, "setStatusBarColor");
+ applyLegacySystemBarColor(window, "setNavigationBarColor");
}
- if (!isDarkTheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- View decorView = window.getDecorView();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ WindowInsetsController controller = window.getInsetsController();
+ if (controller != null) {
+ int appearance =
+ WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
+ WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
+ controller.setSystemBarsAppearance(
+ isDarkTheme ? 0 : appearance,
+ appearance);
+ }
+ } else {
+ int appearance =
+ View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR |
+ View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+ int visibility = decorView.getSystemUiVisibility();
decorView.setSystemUiVisibility(
- decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ isDarkTheme
+ ? visibility & ~appearance
+ : visibility | appearance);
}
} catch (Exception ignored) {}
}
+ private void applyLegacySystemBarColor(Window window, String methodName) {
+ try {
+ window
+ .getClass()
+ .getMethod(methodName, int.class)
+ .invoke(window, colorPrimaryBg);
+ } catch (Exception ignored) {}
+ }
+
private View createMetaRow(String label, String value) {
LinearLayout row = new LinearLayout(this);
row.setOrientation(LinearLayout.HORIZONTAL);
diff --git a/src/plugins/system/android/com/foxdebug/system/System.java b/src/plugins/system/android/com/foxdebug/system/System.java
index c97ef0c77..005d87f6a 100644
--- a/src/plugins/system/android/com/foxdebug/system/System.java
+++ b/src/plugins/system/android/com/foxdebug/system/System.java
@@ -1745,9 +1745,8 @@ private void applySystemBarTheme() {
final Window window = activity.getWindow();
final View decorView = window.getDecorView();
- // Keep Cordova's BackgroundColor flow for API 36+, but also apply the
- // window colors directly so OEM variants do not leave stale system-bar
- // colors behind after a theme switch.
+ // Cordova's SystemBarPlugin owns the system-bar backgrounds. Keep the
+ // window content background and icon contrast synchronized with it.
window.clearFlags(0x04000000 | 0x08000000); // FLAG_TRANSLUCENT_STATUS | FLAG_TRANSLUCENT_NAVIGATION
window.addFlags(0x80000000); // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
@@ -1763,11 +1762,6 @@ private void applySystemBarTheme() {
rootView.setBackgroundColor(this.systemBarColor);
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- window.setStatusBarColor(this.systemBarColor);
- window.setNavigationBarColor(this.systemBarColor);
- }
-
setStatusBarStyle(window);
setNavigationBarStyle(window);
}
diff --git a/tests/unit/androidReleaseConfig.test.js b/tests/unit/androidReleaseConfig.test.js
new file mode 100644
index 000000000..1ac6d008f
--- /dev/null
+++ b/tests/unit/androidReleaseConfig.test.js
@@ -0,0 +1,81 @@
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { test } from "vitest";
+
+const rootDir = fileURLToPath(new URL("../..", import.meta.url));
+
+function read(relativePath) {
+ return fs.readFileSync(path.join(rootDir, relativePath), "utf8");
+}
+
+test("Android release builds stay edge-to-edge safe and conservatively optimized", () => {
+ const theme = read("res/android/values/themes.xml");
+ const buildExtras = read("build-extras.gradle");
+ const proguardRules = read("proguard-rules.pro");
+
+ assert.doesNotMatch(theme, /windowOptOutEdgeToEdgeEnforcement/);
+ assert.match(buildExtras, /release\s*\{[\s\S]*minifyEnabled\s+true/);
+ assert.match(buildExtras, /release\s*\{[\s\S]*shrinkResources\s+true/);
+ assert.match(buildExtras, /proguard-android-optimize\.txt/);
+ assert.match(buildExtras, /rootProject\.file\('\.\.\/\.\.\/proguard-rules\.pro'\)/);
+ assert.doesNotMatch(
+ proguardRules,
+ /^\s*-(?:ignorewarnings|dontoptimize|dontobfuscate)\b/m,
+ );
+ assert.deepEqual(
+ proguardRules.match(/^\s*-dontwarn\s+\S+\s*$/gm)?.map((rule) => rule.trim()),
+ [
+ "-dontwarn java.lang.management.ManagementFactory",
+ "-dontwarn java.lang.management.ThreadInfo",
+ "-dontwarn java.lang.management.ThreadMXBean",
+ ],
+ );
+ assert.doesNotMatch(
+ read("src/plugins/sftp/src/com/foxdebug/sftp/Sftp.java"),
+ /maverick\.threadDump/,
+ );
+
+ const keptPrefixes = [
+ "org.apache.cordova.",
+ "com.foxdebug.",
+ "com.silkimen.",
+ "com.verso.",
+ "admob.plus.",
+ "com.sshtools.",
+ "org.bouncycastle.",
+ ];
+ for (const prefix of keptPrefixes) {
+ assert.ok(
+ proguardRules.includes(`-keep class ${prefix}** { *; }`),
+ `Missing conservative keep rule for ${prefix}`,
+ );
+ }
+ assert.match(proguardRules, /-keep class \*\*\.BuildConfig \{ \*; \}/);
+
+ const pluginRoot = path.join(rootDir, "src/plugins");
+ const featureClasses = fs
+ .readdirSync(pluginRoot, { withFileTypes: true })
+ .filter((entry) => entry.isDirectory())
+ .flatMap((entry) => {
+ const pluginXmlPath = path.join(pluginRoot, entry.name, "plugin.xml");
+ if (!fs.existsSync(pluginXmlPath)) return [];
+ const pluginXml = fs.readFileSync(pluginXmlPath, "utf8");
+ return [...pluginXml.matchAll(/]*>/g)]
+ .map(([param]) => ({
+ name: /\bname=["']([^"']+)["']/.exec(param)?.[1],
+ value: /\bvalue=["']([^"']+)["']/.exec(param)?.[1],
+ }))
+ .filter(({ name, value }) => name === "android-package" && value)
+ .map(({ value }) => value);
+ });
+
+ assert.ok(featureClasses.length > 0, "No Cordova Android feature classes found");
+ for (const className of featureClasses) {
+ assert.ok(
+ keptPrefixes.some((prefix) => className.startsWith(prefix)),
+ `Cordova feature class is not covered by an R8 keep rule: ${className}`,
+ );
+ }
+});