From e26a8252139ad391243d854bbffc60b7d50a0aa1 Mon Sep 17 00:00:00 2001 From: Ajit Kumar Date: Sat, 15 Aug 2026 09:03:24 +0530 Subject: [PATCH] fix(android): harden edge-to-edge release builds --- build-extras.gradle | 11 ++- proguard-rules.pro | 22 +++++ res/android/values/themes.xml | 1 - .../com/foxdebug/browser/BrowserActivity.java | 29 ++++--- .../src/android/CrashActivity.java | 70 ++++++++++++++-- .../android/com/foxdebug/system/System.java | 10 +-- tests/unit/androidReleaseConfig.test.js | 81 +++++++++++++++++++ 7 files changed, 195 insertions(+), 29 deletions(-) create mode 100644 proguard-rules.pro create mode 100644 tests/unit/androidReleaseConfig.test.js 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}`, + ); + } +});