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
11 changes: 10 additions & 1 deletion build-extras.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -12,4 +21,4 @@ configurations {
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk18on'
exclude group: 'org.bouncycastle', module: 'bcprov-jdk18on'
}
}
}
22 changes: 22 additions & 0 deletions proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion res/android/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@
<item name="android:colorAccent">@color/teardrop</item>
<item name="colorControlActivated">@color/teardrop</item>
<item name="android:colorControlActivated">@color/teardrop</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -57,6 +60,7 @@ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

loadThemeColors();
configureEdgeToEdge();
applySystemBarColors();

Intent intent = getIntent();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -261,6 +266,7 @@ public void onClick(View v) {
rootLayout.addView(buttonsLayout);
mainScrollView.addView(rootLayout);
setContentView(mainScrollView);
mainScrollView.requestApplyInsets();
}

private void loadThemeColors() {
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 2 additions & 8 deletions src/plugins/system/android/com/foxdebug/system/System.java
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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);
}
Expand Down
81 changes: 81 additions & 0 deletions tests/unit/androidReleaseConfig.test.js
Original file line number Diff line number Diff line change
@@ -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(/<param\b[^>]*>/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}`,
);
}
});