Skip to content
Merged
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
6 changes: 5 additions & 1 deletion docs/developer-guide/Miscellaneous-Features.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,11 @@ cn1_icon_<lang>[_<country>].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 `<lang>_<COUNTRY>` match first, then fall back to a bare `<lang>` 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 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 *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 `<lang>_<COUNTRY>` match first, then fall back to a bare `<lang>` 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4966,7 +4966,7 @@ public void unzip(InputStream source, File classesDir, File resDir, File sourceD
* the supplied variant receive it.</p>
*/
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) {
Expand All @@ -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<String> undersizedIcons = new ArrayList<String>();
Set<String> languagesWithRegion = new HashSet<String>();
Set<String> languagesWithLanguageOnly = new HashSet<String>();
for (File candidate : candidates) {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,81 @@ private void mergeJars(File dest, File... src) {
}


/**
* Localized launcher icons (cn1_icon_&lt;lang&gt;[_&lt;country&gt;].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<String> 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<String> undersizedIcons = new ArrayList<String>();
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<String> 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.
*/
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -354,6 +429,8 @@ private void createAntProject() throws IOException, LibraryPropertiesException,

}

warnAboutSmallLocalizedIcons(cpElements, codenameOneSettings);

File appExtensionsJar = getAppExtensionsJar();
if (appExtensionsJar != null) {
cpElements.add(appExtensionsJar.getAbsolutePath());
Expand Down
Loading