Make default resources a better match for en-US requests
When locale fallback landed, resources which specified an 'English'
locale started to be considered a better match for en-US, even though
traditionally, apps tend to ship US English resources under their
default locale.
This fixes that, and makes en-US requests match default locales.
Bug: 26756573
Bug: 26789680
Bug: 26803868
Change-Id: I460c276bfc6ddba0439dcdf87497a0aece0fa05d
diff --git a/include/androidfw/LocaleData.h b/include/androidfw/LocaleData.h
index add0ab5..b14829d 100644
--- a/include/androidfw/LocaleData.h
+++ b/include/androidfw/LocaleData.h
@@ -29,6 +29,8 @@
void localeDataComputeScript(char out[4], const char* language, const char* region);
+bool localeDataIsCloseToUsEnglish(const char* region);
+
} // namespace android
#endif // _LIBS_UTILS_LOCALE_DATA_H
diff --git a/libs/androidfw/LocaleData.cpp b/libs/androidfw/LocaleData.cpp
index c0c3ab8..038ef58 100644
--- a/libs/androidfw/LocaleData.cpp
+++ b/libs/androidfw/LocaleData.cpp
@@ -70,13 +70,17 @@
//
// Returns the number of ancestors written in the output, which is always
// at least one.
+//
+// (If 'out' is nullptr, we do everything the same way but we simply don't write
+// any results in 'out'.)
size_t findAncestors(uint32_t* out, ssize_t* stop_list_index,
uint32_t packed_locale, const char* script,
const uint32_t* stop_list, size_t stop_set_length) {
uint32_t ancestor = packed_locale;
size_t count = 0;
do {
- out[count++] = ancestor;
+ if (out != nullptr) out[count] = ancestor;
+ count++;
for (size_t i = 0; i < stop_set_length; i++) {
if (stop_list[i] == ancestor) {
*stop_list_index = (ssize_t) i;
@@ -93,10 +97,9 @@
const char* script,
const uint32_t* request_ancestors,
size_t request_ancestors_count) {
- uint32_t supported_ancestors[MAX_PARENT_DEPTH+1];
ssize_t request_ancestors_index;
const size_t supported_ancestor_count = findAncestors(
- supported_ancestors, &request_ancestors_index,
+ nullptr, &request_ancestors_index,
supported, script,
request_ancestors, request_ancestors_count);
// Since both locales share the same root, there will always be a shared
@@ -198,4 +201,19 @@
}
}
+const uint32_t ENGLISH_STOP_LIST[2] = {
+ 0x656E0000lu, // en
+ 0x656E8400lu, // en-001
+};
+const char ENGLISH_CHARS[2] = {'e', 'n'};
+const char LATIN_CHARS[4] = {'L', 'a', 't', 'n'};
+
+bool localeDataIsCloseToUsEnglish(const char* region) {
+ const uint32_t locale = packLocale(ENGLISH_CHARS, region);
+ ssize_t stop_list_index;
+ findAncestors(nullptr, &stop_list_index, locale, LATIN_CHARS, ENGLISH_STOP_LIST, 2);
+ // A locale is like US English if we see "en" before "en-001" in its ancestor list.
+ return stop_list_index == 0; // 'en' is first in ENGLISH_STOP_LIST
+}
+
} // namespace android
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 71e9c92..1d9fe35 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -2196,7 +2196,32 @@
// The languages of the two resources are not the same. We can only
// assume that one of the two resources matched the request because one
// doesn't have a language and the other has a matching language.
- return (language[0] != 0);
+ //
+ // We consider the one that has the language specified a better match.
+ //
+ // The exception is that we consider no-language resources a better match
+ // for US English and similar locales than locales that are a descendant
+ // of Internatinal English (en-001), since no-language resources are
+ // where the US English resource have traditionally lived for most apps.
+ if (requested->language[0] == 'e' && requested->language[1] == 'n') {
+ if (requested->country[0] == 'U' && requested->country[1] == 'S') {
+ // For US English itself, we consider a no-locale resource a
+ // better match if the other resource has a country other than
+ // US specified.
+ if (language[0] != '\0') {
+ return country[0] == '\0' || (country[0] == 'U' && country[1] == 'S');
+ } else {
+ return !(o.country[0] == '\0' || (o.country[0] == 'U' && o.country[1] == 'S'));
+ }
+ } else if (localeDataIsCloseToUsEnglish(requested->country)) {
+ if (language[0] != '\0') {
+ return localeDataIsCloseToUsEnglish(country);
+ } else {
+ return !localeDataIsCloseToUsEnglish(o.country);
+ }
+ }
+ }
+ return (language[0] != '\0');
}
// If we are here, both the resources have the same non-empty language as
diff --git a/libs/androidfw/tests/ConfigLocale_test.cpp b/libs/androidfw/tests/ConfigLocale_test.cpp
index 1941563..7b38640 100644
--- a/libs/androidfw/tests/ConfigLocale_test.cpp
+++ b/libs/androidfw/tests/ConfigLocale_test.cpp
@@ -592,4 +592,52 @@
EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
}
+// Default resources are considered better matches for US English
+// and US-like English locales than International English locales
+TEST(ConfigLocaleTest, isLocaleBetterThan_UsEnglishIsSpecial) {
+ ResTable_config config1, config2, request;
+
+ fillIn("en", "US", NULL, NULL, &request);
+ fillIn(NULL, NULL, NULL, NULL, &config1);
+ fillIn("en", "001", NULL, NULL, &config2);
+ // default is better than International English
+ EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+ EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+ fillIn("en", "US", NULL, NULL, &request);
+ fillIn(NULL, NULL, NULL, NULL, &config1);
+ fillIn("en", "GB", NULL, NULL, &config2);
+ // default is better than British English
+ EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+ EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+ fillIn("en", "PR", NULL, NULL, &request);
+ fillIn(NULL, NULL, NULL, NULL, &config1);
+ fillIn("en", "001", NULL, NULL, &config2);
+ // Even for Puerto Rico, default is better than International English
+ EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+ EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+ fillIn("en", "US", NULL, NULL, &request);
+ fillIn("en", NULL, NULL, NULL, &config1);
+ fillIn(NULL, NULL, NULL, NULL, &config2);
+ // "English" is better than default, since it's a parent of US English
+ EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+ EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+ fillIn("en", "PR", NULL, NULL, &request);
+ fillIn("en", NULL, NULL, NULL, &config1);
+ fillIn(NULL, NULL, NULL, NULL, &config2);
+ // "English" is better than default, since it's a parent of Puerto Rico English
+ EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+ EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+ fillIn("en", "US", NULL, NULL, &request);
+ fillIn(NULL, NULL, NULL, NULL, &config1);
+ fillIn("en", "PR", NULL, NULL, &config2);
+ // For US English itself, we prefer default to its siblings in the parent tree
+ EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+ EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+}
+
} // namespace android