diff --git a/include/androidfw/ResourceTypes.h b/include/androidfw/ResourceTypes.h index 05411ea12a546..b21977c5a145b 100644 --- a/include/androidfw/ResourceTypes.h +++ b/include/androidfw/ResourceTypes.h @@ -808,6 +808,19 @@ struct ResTable_package uint32_t lastPublicKey; }; +// The most specific locale can consist of: +// +// - a 3 char language code +// - a 3 char region code prefixed by a 'r' +// - a 4 char script code prefixed by a 's' +// - a 8 char variant code prefixed by a 'v' +// +// each separated by a single char separator, which sums up to a total of 24 +// chars, (25 include the string terminator) rounded up to 28 to be 4 byte +// aligned. +#define RESTABLE_MAX_LOCALE_LEN 28 + + /** * Describes a particular resource configuration. */ @@ -828,10 +841,42 @@ struct ResTable_config union { struct { - // \0\0 means "any". Otherwise, en, fr, etc. + // This field can take three different forms: + // - \0\0 means "any". + // + // - Two 7 bit ascii values interpreted as ISO-639-1 language + // codes ('fr', 'en' etc. etc.). The high bit for both bytes is + // zero. + // + // - A single 16 bit little endian packed value representing an + // ISO-639-2 3 letter language code. This will be of the form: + // + // {1, t, t, t, t, t, s, s, s, s, s, f, f, f, f, f} + // + // bit[0, 4] = first letter of the language code + // bit[5, 9] = second letter of the language code + // bit[10, 14] = third letter of the language code. + // bit[15] = 1 always + // + // For backwards compatibility, languages that have unambiguous + // two letter codes are represented in that format. + // + // The layout is always bigendian irrespective of the runtime + // architecture. char language[2]; - // \0\0 means "any". Otherwise, US, CA, etc. + // This field can take three different forms: + // - \0\0 means "any". + // + // - Two 7 bit ascii values interpreted as 2 letter region + // codes ('US', 'GB' etc.). The high bit for both bytes is zero. + // + // - An UN M.49 3 digit region code. For simplicity, these are packed + // in the same manner as the language codes, though we should need + // only 10 bits to represent them, instead of the 15. + // + // The layout is always bigendian irrespective of the runtime + // architecture. char country[2]; }; uint32_t locale; @@ -933,7 +978,7 @@ struct ResTable_config SDKVERSION_ANY = 0 }; - enum { + enum { MINORVERSION_ANY = 0 }; @@ -1006,6 +1051,15 @@ struct ResTable_config uint32_t screenSizeDp; }; + // The ISO-15924 short name for the script corresponding to this + // configuration. (eg. Hant, Latn, etc.). Interpreted in conjunction with + // the locale field + char localeScript[4]; + + // A single BCP-47 variant subtag. Will vary in length between 5 and 8 + // chars. Interpreted in conjunction with the locale field. + char localeVariant[8]; + void copyFromDeviceNoSwap(const ResTable_config& o); void copyFromDtoH(const ResTable_config& o); @@ -1063,7 +1117,33 @@ struct ResTable_config // settings is the requested settings bool match(const ResTable_config& settings) const; - void getLocale(char str[6]) const; + // Get the string representation of the locale component of this + // Config. This will contain the language along with the prefixed script, + // region and variant of this config, separated by underscores. + // + // 'r' is the region prefix, 's' is the script prefix and 'v' is the + // variant prefix. + // + // Example: en_rUS, en_sLatn_rUS, en_vPOSIX. + void getLocale(char str[RESTABLE_MAX_LOCALE_LEN]) const; + // Get the 2 or 3 letter language code of this configuration. Trailing + // bytes are set to '\0'. + size_t unpackLanguage(char language[4]) const; + // Get the 2 or 3 letter language code of this configuration. Trailing + // bytes are set to '\0'. + size_t unpackRegion(char region[4]) const; + + // Sets the language code of this configuration from |language|. If |language| + // is a 2 letter code, the trailing byte is expected to be '\0'. + void packLanguage(const char language[3]); + // Sets the region code of this configuration from |region|. If |region| + // is a 2 letter code, the trailing byte is expected to be '\0'. + void packRegion(const char region[3]); + + // Returns a positive integer if this config is more specific than |o| + // with respect to their locales, a negative integer if |o| is more specific + // and 0 if they're equally specific. + int isLocaleMoreSpecificThan(const ResTable_config &o) const; String8 toString() const; }; diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index 8cc98af96459d..0c356b696d0a5 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -66,11 +66,6 @@ namespace android { // size measured in sizeof(uint32_t) #define IDMAP_HEADER_SIZE (ResTable::IDMAP_HEADER_SIZE_BYTES / sizeof(uint32_t)) -static void printToLogFunc(int32_t cookie, const char* txt) -{ - ALOGV("[cookie=%d] %s", cookie, txt); -} - // Standard C isspace() is only required to look at the low byte of its input, so // produces incorrect results for UTF-16 characters. For safety's sake, assume that // any high-byte UTF-16 code point is not whitespace. @@ -1565,6 +1560,71 @@ void ResTable_config::copyFromDeviceNoSwap(const ResTable_config& o) { } } +/* static */ size_t unpackLanguageOrRegion(const char in[2], const char base, + char out[4]) { + if (in[0] & 0x80) { + // The high bit is "1", which means this is a packed three letter + // language code. + + // The smallest 5 bits of the second char are the first alphabet. + const uint8_t first = in[1] & 0x1f; + // The last three bits of the second char and the first two bits + // of the first char are the second alphabet. + const uint8_t second = ((in[1] & 0xe0) >> 5) + ((in[0] & 0x03) << 3); + // Bits 3 to 7 (inclusive) of the first char are the third alphabet. + const uint8_t third = (in[0] & 0x7c) >> 2; + + out[0] = first + base; + out[1] = second + base; + out[2] = third + base; + out[3] = 0; + + return 3; + } + + if (in[0]) { + memcpy(out, in, 2); + memset(out + 2, 0, 2); + return 2; + } + + memset(out, 0, 4); + return 0; +} + +/* static */ void packLanguageOrRegion(const char in[3], const char base, + char out[2]) { + if (in[2] == 0) { + out[0] = in[0]; + out[1] = in[1]; + } else { + uint8_t first = (in[0] - base) & 0x00ef; + uint8_t second = (in[1] - base) & 0x00ef; + uint8_t third = (in[2] - base) & 0x00ef; + + out[0] = (0x80 | (third << 2) | (second >> 3)); + out[1] = ((second << 5) | first); + } +} + + +void ResTable_config::packLanguage(const char language[3]) { + packLanguageOrRegion(language, 'a', this->language); +} + +void ResTable_config::packRegion(const char region[3]) { + packLanguageOrRegion(region, '0', this->country); +} + +size_t ResTable_config::unpackLanguage(char language[4]) const { + return unpackLanguageOrRegion(this->language, 'a', language); +} + +size_t ResTable_config::unpackRegion(char region[4]) const { + return unpackLanguageOrRegion(this->country, '0', region); +} + + void ResTable_config::copyFromDtoH(const ResTable_config& o) { copyFromDeviceNoSwap(o); size = sizeof(ResTable_config); @@ -1594,10 +1654,30 @@ void ResTable_config::swapHtoD() { screenHeightDp = htods(screenHeightDp); } +/* static */ inline int compareLocales(const ResTable_config &l, const ResTable_config &r) { + if (l.locale != r.locale) { + // NOTE: This is the old behaviour with respect to comparison orders. + // The diff value here doesn't make much sense (given our bit packing scheme) + // but it's stable, and that's all we need. + return l.locale - r.locale; + } + + // The language & region are equal, so compare the scripts and variants. + int script = memcmp(l.localeScript, r.localeScript, sizeof(l.localeScript)); + if (script) { + return script; + } + + // The language, region and script are equal, so compare variants. + // + // This should happen very infrequently (if at all.) + return memcmp(l.localeVariant, r.localeVariant, sizeof(l.localeVariant)); +} + int ResTable_config::compare(const ResTable_config& o) const { int32_t diff = (int32_t)(imsi - o.imsi); if (diff != 0) return diff; - diff = (int32_t)(locale - o.locale); + diff = compareLocales(*this, o); if (diff != 0) return diff; diff = (int32_t)(screenType - o.screenType); if (diff != 0) return diff; @@ -1624,18 +1704,15 @@ int ResTable_config::compareLogical(const ResTable_config& o) const { if (mnc != o.mnc) { return mnc < o.mnc ? -1 : 1; } - if (language[0] != o.language[0]) { - return language[0] < o.language[0] ? -1 : 1; - } - if (language[1] != o.language[1]) { - return language[1] < o.language[1] ? -1 : 1; - } - if (country[0] != o.country[0]) { - return country[0] < o.country[0] ? -1 : 1; + + int diff = compareLocales(*this, o); + if (diff < 0) { + return -1; } - if (country[1] != o.country[1]) { - return country[1] < o.country[1] ? -1 : 1; + if (diff > 0) { + return 1; } + if ((screenLayout & MASK_LAYOUTDIR) != (o.screenLayout & MASK_LAYOUTDIR)) { return (screenLayout & MASK_LAYOUTDIR) < (o.screenLayout & MASK_LAYOUTDIR) ? -1 : 1; } @@ -1682,7 +1759,6 @@ int ResTable_config::diff(const ResTable_config& o) const { int diffs = 0; if (mcc != o.mcc) diffs |= CONFIG_MCC; if (mnc != o.mnc) diffs |= CONFIG_MNC; - if (locale != o.locale) diffs |= CONFIG_LOCALE; if (orientation != o.orientation) diffs |= CONFIG_ORIENTATION; if (density != o.density) diffs |= CONFIG_DENSITY; if (touchscreen != o.touchscreen) diffs |= CONFIG_TOUCHSCREEN; @@ -1697,9 +1773,44 @@ int ResTable_config::diff(const ResTable_config& o) const { if (uiMode != o.uiMode) diffs |= CONFIG_UI_MODE; if (smallestScreenWidthDp != o.smallestScreenWidthDp) diffs |= CONFIG_SMALLEST_SCREEN_SIZE; if (screenSizeDp != o.screenSizeDp) diffs |= CONFIG_SCREEN_SIZE; + + const int diff = compareLocales(*this, o); + if (diff) diffs |= CONFIG_LOCALE; + return diffs; } +int ResTable_config::isLocaleMoreSpecificThan(const ResTable_config& o) const { + if (locale || o.locale) { + if (language[0] != o.language[0]) { + if (!language[0]) return -1; + if (!o.language[0]) return 1; + } + + if (country[0] != o.country[0]) { + if (!country[0]) return -1; + if (!o.country[0]) return 1; + } + } + + // There isn't a well specified "importance" order between variants and + // scripts. We can't easily tell whether, say "en-Latn-US" is more or less + // specific than "en-US-POSIX". + // + // We therefore arbitrarily decide to give priority to variants over + // scripts since it seems more useful to do so. We will consider + // "en-US-POSIX" to be more specific than "en-Latn-US". + + const int score = ((localeScript[0] != 0) ? 1 : 0) + + ((localeVariant[0] != 0) ? 2 : 0); + + const int oScore = ((o.localeScript[0] != 0) ? 1 : 0) + + ((o.localeVariant[0] != 0) ? 2 : 0); + + return score - oScore; + +} + bool ResTable_config::isMoreSpecificThan(const ResTable_config& o) const { // The order of the following tests defines the importance of one // configuration parameter over another. Those tests first are more @@ -1717,14 +1828,13 @@ bool ResTable_config::isMoreSpecificThan(const ResTable_config& o) const { } if (locale || o.locale) { - if (language[0] != o.language[0]) { - if (!language[0]) return false; - if (!o.language[0]) return true; + const int diff = isLocaleMoreSpecificThan(o); + if (diff < 0) { + return false; } - if (country[0] != o.country[0]) { - if (!country[0]) return false; - if (!o.country[0]) return true; + if (diff > 0) { + return true; } } @@ -1860,6 +1970,18 @@ bool ResTable_config::isBetterThan(const ResTable_config& o, } } + if (localeScript[0] || o.localeScript[0]) { + if (localeScript[0] != o.localeScript[0] && requested->localeScript[0]) { + return localeScript[0]; + } + } + + if (localeVariant[0] || o.localeVariant[0]) { + if (localeVariant[0] != o.localeVariant[0] && requested->localeVariant[0]) { + return localeVariant[0]; + } + } + if (screenLayout || o.screenLayout) { if (((screenLayout^o.screenLayout) & MASK_LAYOUTDIR) != 0 && (requested->screenLayout & MASK_LAYOUTDIR)) { @@ -2080,17 +2202,23 @@ bool ResTable_config::match(const ResTable_config& settings) const { } } if (locale != 0) { + // Don't consider the script & variants when deciding matches. + // + // If we two configs differ only in their script or language, they + // can be weeded out in the isMoreSpecificThan test. if (language[0] != 0 && (language[0] != settings.language[0] || language[1] != settings.language[1])) { return false; } + if (country[0] != 0 && (country[0] != settings.country[0] || country[1] != settings.country[1])) { return false; } } + if (screenConfig != 0) { const int layoutDir = screenLayout&MASK_LAYOUTDIR; const int setLayoutDir = settings.screenLayout&MASK_LAYOUTDIR; @@ -2192,16 +2320,42 @@ bool ResTable_config::match(const ResTable_config& settings) const { return true; } -void ResTable_config::getLocale(char str[6]) const { - memset(str, 0, 6); +void ResTable_config::getLocale(char str[RESTABLE_MAX_LOCALE_LEN]) const { + memset(str, 0, RESTABLE_MAX_LOCALE_LEN); + + // This represents the "any" locale value, which has traditionally been + // represented by the empty string. + if (!language[0] && !country[0]) { + return; + } + + size_t charsWritten = 0; if (language[0]) { - str[0] = language[0]; - str[1] = language[1]; - if (country[0]) { - str[2] = '_'; - str[3] = country[0]; - str[4] = country[1]; + unpackLanguage(str); + } + + if (country[0]) { + if (charsWritten) { + str[charsWritten++] = '_'; + str[charsWritten++] = 'r'; + } + charsWritten += unpackRegion(str + charsWritten); + } + + if (localeScript[0]) { + if (charsWritten) { + str[charsWritten++] = '_'; + str[charsWritten++] = '_s'; } + memcpy(str + charsWritten, localeScript, sizeof(localeScript)); + } + + if (localeVariant[0]) { + if (charsWritten) { + str[charsWritten++] = '_'; + str[charsWritten++] = 'v'; + } + memcpy(str + charsWritten, localeVariant, sizeof(localeVariant)); } } @@ -2216,14 +2370,10 @@ String8 ResTable_config::toString() const { if (res.size() > 0) res.append("-"); res.appendFormat("%dmnc", dtohs(mnc)); } - if (language[0] != 0) { - if (res.size() > 0) res.append("-"); - res.append(language, 2); - } - if (country[0] != 0) { - if (res.size() > 0) res.append("-"); - res.append(country, 2); - } + char localeStr[RESTABLE_MAX_LOCALE_LEN]; + getLocale(localeStr); + res.append(localeStr); + if ((screenLayout&MASK_LAYOUTDIR) != 0) { if (res.size() > 0) res.append("-"); switch (screenLayout&ResTable_config::MASK_LAYOUTDIR) { @@ -5002,8 +5152,9 @@ void ResTable::getLocales(Vector* locales) const getConfigurations(&configs); ALOGV("called getConfigurations size=%d", (int)configs.size()); const size_t I = configs.size(); + + char locale[RESTABLE_MAX_LOCALE_LEN]; for (size_t i=0; isize(); size_t j; @@ -5663,9 +5814,9 @@ void ResTable::print(bool inclValues) const printf("mError=0x%x (%s)\n", mError, strerror(mError)); } #if 0 - printf("mParams=%c%c-%c%c,\n", - mParams.language[0], mParams.language[1], - mParams.country[0], mParams.country[1]); + char localeStr[RESTABLE_MAX_LOCALE_LEN]; + mParams.getLocale(localeStr); + printf("mParams=%s,\n" localeStr); #endif size_t pgCount = mPackageGroups.size(); printf("Package Groups (%d)\n", (int)pgCount); @@ -5794,7 +5945,7 @@ void ResTable::print(bool inclValues) const continue; } - uint16_t esize = dtohs(ent->size); + uintptr_t esize = dtohs(ent->size); if ((esize&0x3) != 0) { printf("NON-INTEGER ResTable_entry SIZE: %p\n", (void*)esize); continue; diff --git a/libs/androidfw/tests/Android.mk b/libs/androidfw/tests/Android.mk index 3c55375fc8aaa..977ba80cd49ef 100644 --- a/libs/androidfw/tests/Android.mk +++ b/libs/androidfw/tests/Android.mk @@ -5,7 +5,8 @@ include $(CLEAR_VARS) # Build the unit tests. test_src_files := \ ObbFile_test.cpp \ - ZipUtils_test.cpp + ZipUtils_test.cpp \ + ResourceTypes_test.cpp shared_libraries := \ libandroidfw \ diff --git a/libs/androidfw/tests/ResourceTypes_test.cpp b/libs/androidfw/tests/ResourceTypes_test.cpp new file mode 100644 index 0000000000000..eadfe00330dba --- /dev/null +++ b/libs/androidfw/tests/ResourceTypes_test.cpp @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +namespace android { + +TEST(ResourceTypesTest, ResourceConfig_packAndUnpack2LetterLanguage) { + ResTable_config config; + config.packLanguage("en"); + + EXPECT_EQ('e', config.language[0]); + EXPECT_EQ('n', config.language[1]); + + char out[4] = { 1, 1, 1, 1}; + config.unpackLanguage(out); + EXPECT_EQ('e', out[0]); + EXPECT_EQ('n', out[1]); + EXPECT_EQ(0, out[2]); + EXPECT_EQ(0, out[3]); + + memset(out, 1, sizeof(out)); + config.locale = 0; + config.unpackLanguage(out); + EXPECT_EQ(0, out[0]); + EXPECT_EQ(0, out[1]); + EXPECT_EQ(0, out[2]); + EXPECT_EQ(0, out[3]); +} + +TEST(ResourceTypesTest, ResourceConfig_packAndUnpack2LetterRegion) { + ResTable_config config; + config.packRegion("US"); + + EXPECT_EQ('U', config.country[0]); + EXPECT_EQ('S', config.country[1]); + + char out[4] = { 1, 1, 1, 1}; + config.unpackRegion(out); + EXPECT_EQ('U', out[0]); + EXPECT_EQ('S', out[1]); + EXPECT_EQ(0, out[2]); + EXPECT_EQ(0, out[3]); +} + +TEST(ResourceTypesTest, ResourceConfig_packAndUnpack3LetterLanguage) { + ResTable_config config; + config.packLanguage("eng"); + + // 1-00110-01 101-00100 + EXPECT_EQ(0x99, config.language[0]); + EXPECT_EQ(0xa4, config.language[1]); + + char out[4] = { 1, 1, 1, 1}; + config.unpackLanguage(out); + EXPECT_EQ('e', out[0]); + EXPECT_EQ('n', out[1]); + EXPECT_EQ('g', out[2]); + EXPECT_EQ(0, out[3]); +} + +TEST(ResourceTypesTest, ResourceConfig_packAndUnpack3LetterRegion) { + ResTable_config config; + config.packRegion("419"); + + char out[4] = { 1, 1, 1, 1}; + config.unpackRegion(out); + + EXPECT_EQ('4', out[0]); + EXPECT_EQ('1', out[1]); + EXPECT_EQ('9', out[2]); +} + +/* static */ void fillIn(const char* lang, const char* country, + const char* script, const char* variant, ResTable_config* out) { + memset(out, 0, sizeof(ResTable_config)); + if (lang != NULL) { + out->packLanguage(lang); + } + + if (country != NULL) { + out->packRegion(country); + } + + if (script != NULL) { + memcpy(out->localeScript, script, 4); + } + + if (variant != NULL) { + memcpy(out->localeVariant, variant, strlen(variant)); + } +} + +TEST(ResourceTypesTest, IsMoreSpecificThan) { + ResTable_config l; + ResTable_config r; + + fillIn("en", NULL, NULL, NULL, &l); + fillIn(NULL, NULL, NULL, NULL, &r); + + EXPECT_TRUE(l.isMoreSpecificThan(r)); + EXPECT_FALSE(r.isMoreSpecificThan(l)); + + fillIn("eng", NULL, NULL, NULL, &l); + EXPECT_TRUE(l.isMoreSpecificThan(r)); + EXPECT_FALSE(r.isMoreSpecificThan(l)); + + fillIn("eng", "419", NULL, NULL, &r); + EXPECT_FALSE(l.isMoreSpecificThan(r)); + EXPECT_TRUE(r.isMoreSpecificThan(l)); + + fillIn("en", NULL, NULL, NULL, &l); + fillIn("en", "US", NULL, NULL, &r); + EXPECT_FALSE(l.isMoreSpecificThan(r)); + EXPECT_TRUE(r.isMoreSpecificThan(l)); + + fillIn("en", "US", NULL, NULL, &l); + fillIn("en", "US", "Latn", NULL, &r); + EXPECT_FALSE(l.isMoreSpecificThan(r)); + EXPECT_TRUE(r.isMoreSpecificThan(l)); + + fillIn("en", "US", NULL, NULL, &l); + fillIn("en", "US", NULL, "POSIX", &r); + EXPECT_FALSE(l.isMoreSpecificThan(r)); + EXPECT_TRUE(r.isMoreSpecificThan(l)); + + fillIn("en", "US", "Latn", NULL, &l); + fillIn("en", "US", NULL, "POSIX", &r); + EXPECT_FALSE(l.isMoreSpecificThan(r)); + EXPECT_TRUE(r.isMoreSpecificThan(l)); +} + + +} // namespace android.