Skip to content

Commit 0210813

Browse files
shai-almogclaude
andcommitted
Localized Android icons: prevent sibling-locale fallback
Android's resource resolver (API 24+) walks every child of the parent locale when it can't find an exact or parent-language match, so an ar-PK device with only drawable-ar-rAE/ supplied would match the AE icon. Insert the default app icon into drawable-<lang>/ (and mipmap-*-<lang>/) when only region-qualified variants are provided, so the parent-locale lookup succeeds before sibling matching kicks in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6bc2988 commit 0210813

2 files changed

Lines changed: 69 additions & 22 deletions

File tree

docs/developer-guide/Miscellaneous-Features.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,8 @@ On Android the build generates locale-qualified drawable resources at every dens
469469

470470
No code changes are required—Android's resource framework switches icons when the locale changes.
471471

472+
When you supply a region-qualified icon (such as `cn1_icon_ar_AE.png`) without a matching language-only variant, the build also emits the *default* (non-localized) icon into `drawable-<lang>/` and the matching `mipmap-*-<lang>/` directories. This barrier is required because Android's resource resolver (API 24+) walks every child of the parent locale when it can't find an exact or parent-language match, and would otherwise pick `ar-rAE` for, say, an `ar-PK` device. The barrier short-circuits that lookup so only devices whose region matches the supplied variant receive the localized icon. If you also ship a language-only file (for example `cn1_icon_ar.png`) it is used as the barrier instead, so you keep full control of the fallback icon for Arabic speakers outside AE.
473+
472474
===== iOS behaviour
473475

474476
iOS doesn't localize launcher icons natively, so Codename One wires up https://developer.apple.com/documentation/uikit/uiapplication/2806818-setalternateiconname[alternate app icons] for you:

maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,13 @@
5353
import java.util.ArrayList;
5454

5555
import java.util.HashMap;
56+
import java.util.HashSet;
5657
import java.util.LinkedHashMap;
5758
import java.util.List;
5859
import java.util.Map;
5960
import java.util.Properties;
6061
import java.util.Scanner;
62+
import java.util.Set;
6163
import java.util.StringTokenizer;
6264

6365
import java.util.zip.ZipEntry;
@@ -1865,7 +1867,7 @@ public void usesClassMethod(String cls, String method) {
18651867
createIconFile(new File(drawableDir, "ic_stat_notify.png"), iconImage, 24, 24);
18661868
}
18671869

1868-
processLocalizedIcons(assetsDir, resDir, enableAdaptiveIcons);
1870+
processLocalizedIcons(assetsDir, resDir, enableAdaptiveIcons, iconImage);
18691871
} catch (IOException ex) {
18701872
throw new BuildException("Failed to generate icon files", ex);
18711873
}
@@ -4467,8 +4469,20 @@ public void unzip(InputStream source, File classesDir, File resDir, File sourceD
44674469
* picks up the correct launcher icon at runtime based on the device
44684470
* locale. Source files are removed from assetsDir so they are not shipped
44694471
* as stray assets.
4472+
*
4473+
* <p>When a region-qualified icon (e.g. cn1_icon_ar_AE.png) is supplied
4474+
* without a matching language-only counterpart (cn1_icon_ar.png), the
4475+
* default app icon is also written to drawable-&lt;lang&gt;/ (and the
4476+
* matching mipmap-*-&lt;lang&gt;/ directories). This is required to
4477+
* suppress Android's sibling-locale fallback: starting with API 24 the
4478+
* resource resolver, when it can't find an exact (ar-rPK) or parent (ar)
4479+
* match, walks every child of the parent locale and would otherwise pick
4480+
* ar-rAE for an ar-PK user. Inserting the default icon at the parent
4481+
* level short-circuits that lookup so only devices whose region matches
4482+
* the supplied variant receive it.</p>
44704483
*/
4471-
private void processLocalizedIcons(File assetsDir, File resDir, boolean enableAdaptiveIcons) throws IOException {
4484+
private void processLocalizedIcons(File assetsDir, File resDir, boolean enableAdaptiveIcons,
4485+
BufferedImage defaultIcon) throws IOException {
44724486
File[] candidates = assetsDir.listFiles(new FilenameFilter() {
44734487
@Override
44744488
public boolean accept(File dir, String name) {
@@ -4479,6 +4493,22 @@ public boolean accept(File dir, String name) {
44794493
if (candidates == null || candidates.length == 0) {
44804494
return;
44814495
}
4496+
Set<String> languagesWithRegion = new HashSet<String>();
4497+
Set<String> languagesWithLanguageOnly = new HashSet<String>();
4498+
for (File candidate : candidates) {
4499+
String name = candidate.getName();
4500+
String core = name.substring("cn1_icon_".length(), name.length() - ".png".length());
4501+
String[] parts = core.split("_");
4502+
if (parts.length < 1 || parts[0].length() != 2) {
4503+
continue;
4504+
}
4505+
String lang = parts[0].toLowerCase();
4506+
if (parts.length >= 2 && parts[1].length() == 2) {
4507+
languagesWithRegion.add(lang);
4508+
} else {
4509+
languagesWithLanguageOnly.add(lang);
4510+
}
4511+
}
44824512
for (File candidate : candidates) {
44834513
String name = candidate.getName();
44844514
String core = name.substring("cn1_icon_".length(), name.length() - ".png".length());
@@ -4499,30 +4529,45 @@ public boolean accept(File dir, String name) {
44994529
continue;
45004530
}
45014531

4502-
createIconFile(makeLocalizedDir(resDir, "drawable", qualifier, "icon.png"), img, 128, 128);
4503-
createIconFile(makeLocalizedDir(resDir, "drawable-hdpi", qualifier, "icon.png"), img, 72, 72);
4504-
createIconFile(makeLocalizedDir(resDir, "drawable-ldpi", qualifier, "icon.png"), img, 36, 36);
4505-
createIconFile(makeLocalizedDir(resDir, "drawable-mdpi", qualifier, "icon.png"), img, 48, 48);
4506-
createIconFile(makeLocalizedDir(resDir, "drawable-xhdpi", qualifier, "icon.png"), img, 96, 96);
4507-
createIconFile(makeLocalizedDir(resDir, "drawable-xxhdpi", qualifier, "icon.png"), img, 144, 144);
4508-
createIconFile(makeLocalizedDir(resDir, "drawable-xxxhdpi", qualifier, "icon.png"), img, 192, 192);
4532+
writeLocalizedIconSet(resDir, qualifier, img, enableAdaptiveIcons);
45094533

4510-
if (enableAdaptiveIcons) {
4511-
createIconFile(makeLocalizedDir(resDir, "mipmap-mdpi", qualifier, "ic_launcher.png"), img, 48, 48);
4512-
createIconFile(makeLocalizedDir(resDir, "mipmap-hdpi", qualifier, "ic_launcher.png"), img, 72, 72);
4513-
createIconFile(makeLocalizedDir(resDir, "mipmap-xhdpi", qualifier, "ic_launcher.png"), img, 96, 96);
4514-
createIconFile(makeLocalizedDir(resDir, "mipmap-xxhdpi", qualifier, "ic_launcher.png"), img, 144, 144);
4515-
createIconFile(makeLocalizedDir(resDir, "mipmap-xxxhdpi", qualifier, "ic_launcher.png"), img, 192, 192);
4534+
candidate.delete();
4535+
log("Registered localized launcher icon for qualifier " + qualifier + " (" + name + ")");
4536+
}
45164537

4517-
createIconFile(makeLocalizedDir(resDir, "mipmap-mdpi", qualifier, "ic_launcher_foreground.png"), img, 108, 108);
4518-
createIconFile(makeLocalizedDir(resDir, "mipmap-hdpi", qualifier, "ic_launcher_foreground.png"), img, 162, 162);
4519-
createIconFile(makeLocalizedDir(resDir, "mipmap-xhdpi", qualifier, "ic_launcher_foreground.png"), img, 216, 216);
4520-
createIconFile(makeLocalizedDir(resDir, "mipmap-xxhdpi", qualifier, "ic_launcher_foreground.png"), img, 324, 324);
4521-
createIconFile(makeLocalizedDir(resDir, "mipmap-xxxhdpi", qualifier, "ic_launcher_foreground.png"), img, 432, 432);
4538+
for (String lang : languagesWithRegion) {
4539+
if (languagesWithLanguageOnly.contains(lang)) {
4540+
continue;
45224541
}
4542+
String qualifier = "-" + lang;
4543+
writeLocalizedIconSet(resDir, qualifier, defaultIcon, enableAdaptiveIcons);
4544+
log("Registered default-icon barrier for qualifier " + qualifier
4545+
+ " to suppress Android sibling-locale fallback");
4546+
}
4547+
}
45234548

4524-
candidate.delete();
4525-
log("Registered localized launcher icon for qualifier " + qualifier + " (" + name + ")");
4549+
private void writeLocalizedIconSet(File resDir, String qualifier, BufferedImage img,
4550+
boolean enableAdaptiveIcons) throws IOException {
4551+
createIconFile(makeLocalizedDir(resDir, "drawable", qualifier, "icon.png"), img, 128, 128);
4552+
createIconFile(makeLocalizedDir(resDir, "drawable-hdpi", qualifier, "icon.png"), img, 72, 72);
4553+
createIconFile(makeLocalizedDir(resDir, "drawable-ldpi", qualifier, "icon.png"), img, 36, 36);
4554+
createIconFile(makeLocalizedDir(resDir, "drawable-mdpi", qualifier, "icon.png"), img, 48, 48);
4555+
createIconFile(makeLocalizedDir(resDir, "drawable-xhdpi", qualifier, "icon.png"), img, 96, 96);
4556+
createIconFile(makeLocalizedDir(resDir, "drawable-xxhdpi", qualifier, "icon.png"), img, 144, 144);
4557+
createIconFile(makeLocalizedDir(resDir, "drawable-xxxhdpi", qualifier, "icon.png"), img, 192, 192);
4558+
4559+
if (enableAdaptiveIcons) {
4560+
createIconFile(makeLocalizedDir(resDir, "mipmap-mdpi", qualifier, "ic_launcher.png"), img, 48, 48);
4561+
createIconFile(makeLocalizedDir(resDir, "mipmap-hdpi", qualifier, "ic_launcher.png"), img, 72, 72);
4562+
createIconFile(makeLocalizedDir(resDir, "mipmap-xhdpi", qualifier, "ic_launcher.png"), img, 96, 96);
4563+
createIconFile(makeLocalizedDir(resDir, "mipmap-xxhdpi", qualifier, "ic_launcher.png"), img, 144, 144);
4564+
createIconFile(makeLocalizedDir(resDir, "mipmap-xxxhdpi", qualifier, "ic_launcher.png"), img, 192, 192);
4565+
4566+
createIconFile(makeLocalizedDir(resDir, "mipmap-mdpi", qualifier, "ic_launcher_foreground.png"), img, 108, 108);
4567+
createIconFile(makeLocalizedDir(resDir, "mipmap-hdpi", qualifier, "ic_launcher_foreground.png"), img, 162, 162);
4568+
createIconFile(makeLocalizedDir(resDir, "mipmap-xhdpi", qualifier, "ic_launcher_foreground.png"), img, 216, 216);
4569+
createIconFile(makeLocalizedDir(resDir, "mipmap-xxhdpi", qualifier, "ic_launcher_foreground.png"), img, 324, 324);
4570+
createIconFile(makeLocalizedDir(resDir, "mipmap-xxxhdpi", qualifier, "ic_launcher_foreground.png"), img, 432, 432);
45264571
}
45274572
}
45284573

0 commit comments

Comments
 (0)