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.