diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java
index 05986ae6..a56ec464 100644
--- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java
+++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java
@@ -26,9 +26,7 @@ public class ResConfigFlags {
public final short mnc;
public final char[] language;
- public final char[] country;
-
- public final short layoutDirection;
+ public final char[] region;
public final byte orientation;
public final byte touchscreen;
@@ -50,6 +48,9 @@ public class ResConfigFlags {
public final short screenWidthDp;
public final short screenHeightDp;
+ private final char[] localeScript;
+ private final char[] localeVariant;
+
public final boolean isInvalid;
private final String mQualifiers;
@@ -58,8 +59,7 @@ public class ResConfigFlags {
mcc = 0;
mnc = 0;
language = new char[] { '\00', '\00' };
- country = new char[] { '\00', '\00' };
- layoutDirection = SCREENLAYOUT_LAYOUTDIR_ANY;
+ region = new char[] { '\00', '\00' };
orientation = ORIENTATION_ANY;
touchscreen = TOUCHSCREEN_ANY;
density = DENSITY_DEFAULT;
@@ -74,17 +74,20 @@ public class ResConfigFlags {
smallestScreenWidthDp = 0;
screenWidthDp = 0;
screenHeightDp = 0;
+ localeScript = new char[] { '\00', '\00', '\00', '\00' };
+ localeVariant = new char[] { '\00', '\00', '\00', '\00', '\00', '\00', '\00', '\00' };
isInvalid = false;
mQualifiers = "";
}
public ResConfigFlags(short mcc, short mnc, char[] language,
- char[] country, short layoutDirection, byte orientation,
+ char[] region, byte orientation,
byte touchscreen, int density, byte keyboard, byte navigation,
byte inputFlags, short screenWidth, short screenHeight,
short sdkVersion, byte screenLayout, byte uiMode,
short smallestScreenWidthDp, short screenWidthDp,
- short screenHeightDp, boolean isInvalid) {
+ short screenHeightDp, char[] localeScript, char[] localeVariant,
+ boolean isInvalid) {
if (orientation < 0 || orientation > 3) {
LOGGER.warning("Invalid orientation value: " + orientation);
orientation = 0;
@@ -114,8 +117,7 @@ public class ResConfigFlags {
this.mcc = mcc;
this.mnc = mnc;
this.language = language;
- this.country = country;
- this.layoutDirection = layoutDirection;
+ this.region = region;
this.orientation = orientation;
this.touchscreen = touchscreen;
this.density = density;
@@ -130,6 +132,8 @@ public class ResConfigFlags {
this.smallestScreenWidthDp = smallestScreenWidthDp;
this.screenWidthDp = screenWidthDp;
this.screenHeightDp = screenHeightDp;
+ this.localeScript = localeScript;
+ this.localeVariant = localeVariant;
this.isInvalid = isInvalid;
mQualifiers = generateQualifiers();
}
@@ -155,12 +159,8 @@ public class ResConfigFlags {
ret.append("-mnc00");
}
}
- if (language[0] != '\00') {
- ret.append('-').append(language);
- if (country[0] != '\00') {
- ret.append("-r").append(country);
- }
- }
+ ret.append(getLocaleString());
+
switch (screenLayout & MASK_LAYOUTDIR) {
case SCREENLAYOUT_LAYOUTDIR_RTL:
ret.append("-ldrtl");
@@ -369,6 +369,51 @@ public class ResConfigFlags {
return 0;
}
+ private String getLocaleString() {
+ StringBuilder sb = new StringBuilder();
+
+ // check for old style non BCP47 tags
+ // allows values-xx-rXX, values-xx, values-xxx-rXX
+ // denies values-xxx, anything else
+ if (language[0] != '\00' && localeScript.length == 0 && localeVariant.length == 0 &&
+ (region.length != 3 && language.length != 3) ||
+ (language.length == 3 && region.length == 2 && region[0] != '\00' &&
+ localeScript.length == 0 && localeVariant.length == 0)) {
+
+ sb.append("-").append(language);
+ if (region[0] != '\00') {
+ sb.append("-r").append(region);
+ }
+ } else { // BCP47
+ if (language[0] == '\00' && region[0] == '\00') {
+ return sb.toString(); // early return, no language or region
+ }
+ sb.append("-b+");
+ if (language[0] != '\00') {
+ sb.append(language);
+ }
+ if (localeScript.length == 4) {
+ sb.append("+").append(localeScript);
+ }
+ if ((region.length == 2 || region.length == 3) && region[0] != '\00') {
+ sb.append("+").append(region);
+ }
+ if (localeVariant.length >= 5) {
+ sb.append("+").append(toUpper(localeVariant));
+ }
+ }
+ return sb.toString();
+ }
+
+ private String toUpper(char[] character) {
+ StringBuilder sb = new StringBuilder();
+ for (char ch: character) {
+ sb.append(Character.toUpperCase(ch));
+ }
+ return sb.toString();
+ }
+
+
@Override
public String toString() {
return !getQualifiers().equals("") ? getQualifiers() : "[DEFAULT]";
diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java
index 622e9c30..57caf4ad 100644
--- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java
+++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java
@@ -256,8 +256,8 @@ public class ARSCDecoder {
short mcc = mIn.readShort();
short mnc = mIn.readShort();
- char[] language = new char[] { (char) mIn.readByte(), (char) mIn.readByte() };
- char[] country = new char[] { (char) mIn.readByte(), (char) mIn.readByte() };
+ char[] language = this.unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), 'a');
+ char[] country = this.unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), '0');
byte orientation = mIn.readByte();
byte touchscreen = mIn.readByte();
@@ -291,9 +291,11 @@ public class ARSCDecoder {
screenHeightDp = mIn.readShort();
}
- short layoutDirection = 0;
- if (size >= 38) {
- layoutDirection = mIn.readShort();
+ char[] localeScript = {'\00'};
+ char[] localeVariant = {'\00'};
+ if (size >= 48) {
+ localeScript = this.readScriptOrVariantChar(4).toCharArray();
+ localeVariant = this.readScriptOrVariantChar(8).toCharArray();
}
int exceedingSize = size - KNOWN_CONFIG_BYTES;
@@ -313,11 +315,40 @@ public class ARSCDecoder {
}
}
- return new ResConfigFlags(mcc, mnc, language, country, layoutDirection,
+ return new ResConfigFlags(mcc, mnc, language, country,
orientation, touchscreen, density, keyboard, navigation,
inputFlags, screenWidth, screenHeight, sdkVersion,
screenLayout, uiMode, smallestScreenWidthDp, screenWidthDp,
- screenHeightDp, isInvalid);
+ screenHeightDp, localeScript, localeVariant, isInvalid);
+ }
+
+ private char[] unpackLanguageOrRegion(byte in0, byte in1, char base) throws AndrolibException {
+ // check high bit, if so we have a packed 3 letter code
+ if (((in0 >> 7) & 1) == 1) {
+ int first = in1 & 0x1F;
+ int second = ((in1 & 0xE0) >> 5) + ((in0 & 0x03) << 3);
+ int third = (in0 & 0x7C) >> 2;
+
+ // since this function handles languages & regions, we add the value(s) to the base char
+ // which is usually 'a' or '0' depending on language or region.
+ return new char[] { (char) (first + base), (char) (second + base), (char) (third + base) };
+ }
+ return new char[] { (char) in0, (char) in1 };
+ }
+
+ private String readScriptOrVariantChar(int length) throws AndrolibException, IOException {
+ StringBuilder string = new StringBuilder(16);
+
+ while(length-- != 0) {
+ short ch = mIn.readByte();
+ if (ch == 0) {
+ break;
+ }
+ string.append((char) ch);
+ }
+ mIn.skipBytes(length);
+
+ return string.toString();
}
private void addMissingResSpecs() throws AndrolibException {
@@ -416,7 +447,7 @@ public class ARSCDecoder {
}
private static final Logger LOGGER = Logger.getLogger(ARSCDecoder.class.getName());
- private static final int KNOWN_CONFIG_BYTES = 38;
+ private static final int KNOWN_CONFIG_BYTES = 48;
public static class ARSCData {
diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java
index 4ee4e694..11f7c10a 100644
--- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java
+++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java
@@ -182,6 +182,51 @@ public class BuildAndDecodeTest {
compareValuesFiles("values-watch/strings.xml");
}
+ @Test
+ public void packed3CharsTest() throws BrutException, IOException {
+ compareValuesFiles("values-ast-rES/strings.xml");
+ }
+
+ @Test
+ public void rightToLeftTest() throws BrutException, IOException {
+ compareValuesFiles("values-ldrtl/strings.xml");
+ }
+
+ @Test
+ public void scriptBcp47Test() throws BrutException, IOException {
+ compareValuesFiles("values-b+en+Latn+US/strings.xml");
+ }
+
+ @Test
+ public void threeLetterLangBcp47Test() throws BrutException, IOException {
+ compareValuesFiles("values-b+ast/strings.xml");
+ }
+
+ @Test
+ public void twoLetterLangBcp47Test() throws BrutException, IOException {
+ compareValuesFiles("values-en-rUS/strings.xml");
+ }
+
+ @Test
+ public void variantBcp47Test() throws BrutException, IOException {
+ compareValuesFiles("values-b+en+US+POSIX/strings.xml");
+ }
+
+ @Test
+ public void fourpartBcp47Test() throws BrutException, IOException {
+ compareValuesFiles("values-b+ast+Latn+IT+AREVELA/strings.xml");
+ }
+
+ @Test
+ public void RegionLocaleBcp47Test() throws BrutException, IOException {
+ compareValuesFiles("values-b+en+Latn+419/strings.xml");
+ }
+
+ @Test
+ public void numericalRegionBcp47Test() throws BrutException, IOException {
+ compareValuesFiles("values-b+eng+419/strings.xml");
+ }
+
@Test
public void drawableNoDpiTest() throws BrutException, IOException {
compareResFolder("drawable-nodpi");
diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ast-rES/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ast-rES/strings.xml
new file mode 100644
index 00000000..35eaf5ef
--- /dev/null
+++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ast-rES/strings.xml
@@ -0,0 +1,4 @@
+
+
+ test1
+
\ No newline at end of file
diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast+Latn+IT+AREVELA/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast+Latn+IT+AREVELA/strings.xml
new file mode 100644
index 00000000..35eaf5ef
--- /dev/null
+++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast+Latn+IT+AREVELA/strings.xml
@@ -0,0 +1,4 @@
+
+
+ test1
+
\ No newline at end of file
diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast/strings.xml
new file mode 100644
index 00000000..35eaf5ef
--- /dev/null
+++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+ast/strings.xml
@@ -0,0 +1,4 @@
+
+
+ test1
+
\ No newline at end of file
diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+419/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+419/strings.xml
new file mode 100644
index 00000000..35eaf5ef
--- /dev/null
+++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+419/strings.xml
@@ -0,0 +1,4 @@
+
+
+ test1
+
\ No newline at end of file
diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+US/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+US/strings.xml
new file mode 100644
index 00000000..35eaf5ef
--- /dev/null
+++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+Latn+US/strings.xml
@@ -0,0 +1,4 @@
+
+
+ test1
+
\ No newline at end of file
diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+US+POSIX/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+US+POSIX/strings.xml
new file mode 100644
index 00000000..35eaf5ef
--- /dev/null
+++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+en+US+POSIX/strings.xml
@@ -0,0 +1,4 @@
+
+
+ test1
+
\ No newline at end of file
diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+eng+419/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+eng+419/strings.xml
new file mode 100644
index 00000000..35eaf5ef
--- /dev/null
+++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-b+eng+419/strings.xml
@@ -0,0 +1,4 @@
+
+
+ test1
+
\ No newline at end of file
diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-en-rUS/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-en-rUS/strings.xml
new file mode 100644
index 00000000..35eaf5ef
--- /dev/null
+++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-en-rUS/strings.xml
@@ -0,0 +1,4 @@
+
+
+ test1
+
\ No newline at end of file
diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ldrtl/strings.xml b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ldrtl/strings.xml
new file mode 100644
index 00000000..35eaf5ef
--- /dev/null
+++ b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-ldrtl/strings.xml
@@ -0,0 +1,4 @@
+
+
+ test1
+
\ No newline at end of file