From b9053bf984328e61b484fad0800b60b3a48df347 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Tue, 9 Jun 2026 19:20:18 +0300
Subject: [PATCH 1/2] Fail the build on undersized localized launcher icons
Localized launcher icons (cn1_icon_[_].png) are scaled up
to the largest launcher density at build time, so a low-resolution source
produces a blurry icon. Two related problems were reported on Android:
1. A small localized icon was upscaled and shipped blurry with no warning.
2. Maven copies these resources into target/classes incrementally and does
not delete stale copies when a source icon is removed/renamed, so an old
low-res icon kept getting bundled until 'mvn clean' was run manually.
This adds a hard build failure (not just a warning) when a localized icon
is smaller than the size it would be upscaled to (192px normally, 432px for
the adaptive foreground), so a soft icon can no longer reach production:
- AndroidGradleBuilder.processLocalizedIcons: collects undersized icons and
throws a BuildException with a clear message before producing the APK.
- CN1BuildMojo: scans the compiled output (target/classes) that is about to
be bundled and sent to the build server and fails with a MojoFailureException,
pointing at stale copies and recommending 'mvn clean'.
Docs updated with the resolution recommendation and the stale-resource note.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../Miscellaneous-Features.asciidoc | 6 +-
.../builders/AndroidGradleBuilder.java | 22 +++++-
.../com/codename1/maven/CN1BuildMojo.java | 79 ++++++++++++++++++-
3 files changed, 104 insertions(+), 3 deletions(-)
diff --git a/docs/developer-guide/Miscellaneous-Features.asciidoc b/docs/developer-guide/Miscellaneous-Features.asciidoc
index daebcb7fb2..a4a2671cc3 100644
--- a/docs/developer-guide/Miscellaneous-Features.asciidoc
+++ b/docs/developer-guide/Miscellaneous-Features.asciidoc
@@ -458,7 +458,11 @@ cn1_icon_[_].png
* `cn1_icon_en_GB.png`: British English
* `cn1_icon_es_MX.png`: Mexican Spanish
-Supply a square source image at least 432×432 pixels (the largest size emitted for Android adaptive icons); the build resizes it to every target density. The default app icon continues to be controlled by your `codenameone_settings.properties` file and is used whenever the device locale doesn't match any of the localized variants. At runtime the builders look for a `_` match first, then fall back to a bare `` match. Providing both (for example `cn1_icon_en.png` plus `cn1_icon_en_GB.png`) lets you give British users a country-specific icon while every other English locale still receives the generic English icon.
+Supply a square source image at least 432×432 pixels (the largest size emitted for Android adaptive icons; 1024×1024 is recommended, matching the main app icon); the build resizes it to every target density. A smaller source is upscaled and will look blurry — the build now logs a warning when it detects an undersized localized icon. The default app icon continues to be controlled by your `codenameone_settings.properties` file and is used whenever the device locale doesn't match any of the localized variants.
+
+NOTE: These icons live under `src/main/resources` and Maven copies them into `target/classes` incrementally — it does *not* delete the copy when you remove or rename the source file. If you replace a localized icon and still see the old (often blurry) one, run `mvn clean` to clear the stale resource from `target/classes` before rebuilding. The build also warns when it finds an undersized `cn1_icon_*.png` in the compiled output that is about to be bundled and sent to the build server.
+
+At runtime the builders look for a `_` match first, then fall back to a bare `` match. Providing both (for example `cn1_icon_en.png` plus `cn1_icon_en_GB.png`) lets you give British users a country-specific icon while every other English locale still receives the generic English icon.
===== Android behavior
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
index b14cf52869..0ada2863ad 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
@@ -4966,7 +4966,7 @@ public void unzip(InputStream source, File classesDir, File resDir, File sourceD
* the supplied variant receive it.
*/
private void processLocalizedIcons(File assetsDir, File resDir, boolean enableAdaptiveIcons,
- BufferedImage defaultIcon) throws IOException {
+ BufferedImage defaultIcon) throws IOException, BuildException {
File[] candidates = assetsDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
@@ -4977,6 +4977,12 @@ public boolean accept(File dir, String name) {
if (candidates == null || candidates.length == 0) {
return;
}
+ // Icons smaller than the largest launcher density would have to be upscaled and would
+ // render blurry. Collect any such offenders and fail the build at the end so a soft icon
+ // never reaches production. The threshold matches the largest size writeLocalizedIconSet
+ // emits: 192px normally, 432px for the adaptive foreground.
+ int largestTarget = enableAdaptiveIcons ? 432 : 192;
+ List undersizedIcons = new ArrayList();
Set languagesWithRegion = new HashSet();
Set languagesWithLanguageOnly = new HashSet();
for (File candidate : candidates) {
@@ -5013,12 +5019,26 @@ public boolean accept(File dir, String name) {
continue;
}
+ // Anything smaller than the largest launcher density would be upscaled and render
+ // blurry on high-density devices. Record it and fail the build after the loop rather
+ // than silently shipping a soft icon to production.
+ if (img.getWidth() < largestTarget || img.getHeight() < largestTarget) {
+ undersizedIcons.add(name + " (" + img.getWidth() + "x" + img.getHeight() + "px)");
+ }
+
writeLocalizedIconSet(resDir, qualifier, img, enableAdaptiveIcons);
candidate.delete();
log("Registered localized launcher icon for qualifier " + qualifier + " (" + name + ")");
}
+ if (!undersizedIcons.isEmpty()) {
+ throw new BuildException("The following localized launcher icon(s) are smaller than "
+ + largestTarget + "x" + largestTarget + "px and would be upscaled to a blurry icon: "
+ + undersizedIcons + ". Supply each localized icon at no less than " + largestTarget + "x"
+ + largestTarget + "px (1024x1024 is recommended, matching the main app icon).");
+ }
+
for (String lang : languagesWithRegion) {
if (languagesWithLanguageOnly.contains(lang)) {
continue;
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java
index 13e9f6a621..942b4748d7 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java
@@ -160,6 +160,81 @@ private void mergeJars(File dest, File... src) {
}
+ /**
+ * Localized launcher icons (cn1_icon_<lang>[_<country>].png) are scaled up to the
+ * largest launcher density by the build server, so a low-resolution source produces a
+ * blurry icon. Maven copies these into the build output (target/classes) with its
+ * incremental resource plugin, which also leaves stale copies behind when a source icon
+ * is removed or replaced (only {@code mvn clean} clears them). Scan the compiled output
+ * directories that will be bundled and sent to the build server and warn about any
+ * localized icon that is too small to render sharply -- this catches both an undersized
+ * new icon and an outdated low-resolution one lingering in target/classes.
+ *
+ * @param classpathElements the compile classpath; directory entries are the project /
+ * module {@code target/classes} folders that get bundled.
+ * @param codenameOneSettings the project's codenameone_settings.properties, used to detect
+ * whether adaptive icons are enabled (which raises the target size).
+ */
+ private void warnAboutSmallLocalizedIcons(List classpathElements, File codenameOneSettings)
+ throws MojoFailureException {
+ int largestTarget = 192;
+ try {
+ Properties settings = new Properties();
+ try (FileInputStream fis = new FileInputStream(codenameOneSettings)) {
+ settings.load(fis);
+ }
+ if ("true".equals(settings.getProperty("codename1.arg.android.enableAdaptiveIcons", "false").trim())) {
+ largestTarget = 432;
+ }
+ } catch (IOException ex) {
+ getLog().debug("Could not read " + codenameOneSettings + " to determine adaptive icon setting", ex);
+ }
+ List undersizedIcons = new ArrayList();
+ for (String element : classpathElements) {
+ File dir = new File(element);
+ if (dir.isDirectory()) {
+ collectSmallLocalizedIcons(dir, largestTarget, undersizedIcons);
+ }
+ }
+ if (!undersizedIcons.isEmpty()) {
+ throw new MojoFailureException("The following localized launcher icon(s) are smaller than "
+ + largestTarget + "x" + largestTarget + "px and would be upscaled to a blurry icon in the "
+ + "production build:\n " + String.join("\n ", undersizedIcons)
+ + "\nSupply each localized icon at no less than " + largestTarget + "x" + largestTarget
+ + "px (1024x1024 recommended, matching the main app icon). NOTE: if you recently replaced an icon, "
+ + "an offending copy may be a stale resource left in target/classes -- run 'mvn clean' to clear it.");
+ }
+ }
+
+ private void collectSmallLocalizedIcons(File dir, int largestTarget, List undersizedIcons) {
+ File[] children = dir.listFiles();
+ if (children == null) {
+ return;
+ }
+ for (File child : children) {
+ if (child.isDirectory()) {
+ collectSmallLocalizedIcons(child, largestTarget, undersizedIcons);
+ continue;
+ }
+ String lower = child.getName().toLowerCase();
+ if (!lower.startsWith("cn1_icon_") || !lower.endsWith(".png")) {
+ continue;
+ }
+ try {
+ BufferedImage img = ImageIO.read(child);
+ if (img == null) {
+ undersizedIcons.add(child + " (not a valid PNG image)");
+ continue;
+ }
+ if (img.getWidth() < largestTarget || img.getHeight() < largestTarget) {
+ undersizedIcons.add(child + " (" + img.getWidth() + "x" + img.getHeight() + "px)");
+ }
+ } catch (IOException ex) {
+ getLog().debug("Could not read localized icon " + child + " to check its resolution", ex);
+ }
+ }
+ }
+
/**
* The dependency scopes to include in the jar file that is sent to the build server.
*/
@@ -323,7 +398,7 @@ private boolean isLocalBuildTarget(String buildTarget) {
|| BUILD_TARGET_WINDOWS_NATIVE.equals(buildTarget));
}
- private void createAntProject() throws IOException, LibraryPropertiesException, MojoExecutionException {
+ private void createAntProject() throws IOException, LibraryPropertiesException, MojoExecutionException, MojoFailureException {
File cn1dir = new File(project.getBuild().getDirectory() + File.separator + "codenameone");
File antProject = new File(cn1dir, "antProject");
@@ -354,6 +429,8 @@ private void createAntProject() throws IOException, LibraryPropertiesException,
}
+ warnAboutSmallLocalizedIcons(cpElements, codenameOneSettings);
+
File appExtensionsJar = getAppExtensionsJar();
if (appExtensionsJar != null) {
cpElements.add(appExtensionsJar.getAbsolutePath());
From 7be6fcbe27f5f47ea31bfc97e580c367821b090e Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Tue, 9 Jun 2026 20:19:26 +0300
Subject: [PATCH 2/2] docs: fix Vale contraction lint + reflect hard-failure
behavior
Use contractions (doesn't / that's) to satisfy the Microsoft.Contractions
rule in the developer-guide quality gate, and update the wording from
"logs a warning" to "fails the build" to match the new behavior.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
docs/developer-guide/Miscellaneous-Features.asciidoc | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/developer-guide/Miscellaneous-Features.asciidoc b/docs/developer-guide/Miscellaneous-Features.asciidoc
index a4a2671cc3..d35fc37d55 100644
--- a/docs/developer-guide/Miscellaneous-Features.asciidoc
+++ b/docs/developer-guide/Miscellaneous-Features.asciidoc
@@ -458,9 +458,9 @@ cn1_icon_[_].png
* `cn1_icon_en_GB.png`: British English
* `cn1_icon_es_MX.png`: Mexican Spanish
-Supply a square source image at least 432×432 pixels (the largest size emitted for Android adaptive icons; 1024×1024 is recommended, matching the main app icon); the build resizes it to every target density. A smaller source is upscaled and will look blurry — the build now logs a warning when it detects an undersized localized icon. The default app icon continues to be controlled by your `codenameone_settings.properties` file and is used whenever the device locale doesn't match any of the localized variants.
+Supply a square source image at least 432×432 pixels (the largest size emitted for Android adaptive icons; 1024×1024 is recommended, matching the main app icon); the build resizes it to every target density. A smaller source would be upscaled and look blurry, so the build now fails with a clear error when it detects an undersized localized icon rather than shipping a soft one. The default app icon continues to be controlled by your `codenameone_settings.properties` file and is used whenever the device locale doesn't match any of the localized variants.
-NOTE: These icons live under `src/main/resources` and Maven copies them into `target/classes` incrementally — it does *not* delete the copy when you remove or rename the source file. If you replace a localized icon and still see the old (often blurry) one, run `mvn clean` to clear the stale resource from `target/classes` before rebuilding. The build also warns when it finds an undersized `cn1_icon_*.png` in the compiled output that is about to be bundled and sent to the build server.
+NOTE: These icons live under `src/main/resources` and Maven copies them into `target/classes` incrementally — it *doesn't* delete the copy when you remove or rename the source file. If you replace a localized icon and still see the old (often blurry) one, run `mvn clean` to clear the stale resource from `target/classes` before rebuilding. The build also fails when it finds an undersized `cn1_icon_*.png` in the compiled output that's about to be bundled and sent to the build server.
At runtime the builders look for a `_` match first, then fall back to a bare `` match. Providing both (for example `cn1_icon_en.png` plus `cn1_icon_en_GB.png`) lets you give British users a country-specific icon while every other English locale still receives the generic English icon.