diff options
author | Jake Hamby <jhamby@google.com> | 2011-05-05 13:31:03 -0700 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2011-05-05 13:31:03 -0700 |
commit | 18cc814c94edb188b73a92f0d34878d9dc054ff6 (patch) | |
tree | 9e68e5068c56cc4d4ce74a2a69a4ccc6a7dd6b9e /telephony/tests/telephonytests/src | |
parent | 8a14d8b9c03c611cabde5743cfdcc9ff51314550 (diff) | |
parent | 4d53cb02168fe35104d7e644dd9b3efd8ca4c91b (diff) | |
download | frameworks_base-18cc814c94edb188b73a92f0d34878d9dc054ff6.zip frameworks_base-18cc814c94edb188b73a92f0d34878d9dc054ff6.tar.gz frameworks_base-18cc814c94edb188b73a92f0d34878d9dc054ff6.tar.bz2 |
am 4d53cb02: Merge "Enable support for SMS national language shift tables." into gingerbread
* commit '4d53cb02168fe35104d7e644dd9b3efd8ca4c91b':
Enable support for SMS national language shift tables.
Diffstat (limited to 'telephony/tests/telephonytests/src')
3 files changed, 662 insertions, 44 deletions
diff --git a/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmAlphabetTest.java b/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmAlphabetTest.java index 3a9c511..e83a822 100644 --- a/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmAlphabetTest.java +++ b/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmAlphabetTest.java @@ -20,7 +20,6 @@ import junit.framework.TestCase; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.SmallTest; -import android.test.suitebuilder.annotation.Suppress; public class GsmAlphabetTest extends TestCase { @@ -38,20 +37,21 @@ public class GsmAlphabetTest extends TestCase { String message = "aaaaaaaaaabbbbbbbbbbcccccccccc"; byte[] userData = GsmAlphabet.stringToGsm7BitPackedWithHeader(message, - SmsHeader.toByteArray(header)); - int septetCount = GsmAlphabet.countGsmSeptets(message, false); + SmsHeader.toByteArray(header), 0, 0); + int septetCount = GsmAlphabet.countGsmSeptetsUsingTables(message, true, 0, 0); String parsedMessage = GsmAlphabet.gsm7BitPackedToString( - userData, SmsHeader.toByteArray(header).length+2, septetCount, 1); + userData, SmsHeader.toByteArray(header).length+2, septetCount, 1, 0, 0); assertEquals(message, parsedMessage); } // TODO: This method should *really* be a series of individual test methods. - @LargeTest + // However, it's a SmallTest because it executes quickly. + @SmallTest public void testBasic() throws Exception { // '@' maps to char 0 assertEquals(0, GsmAlphabet.charToGsm('@')); - // `a (a with grave accent) maps to last GSM charater + // `a (a with grave accent) maps to last GSM character assertEquals(0x7f, GsmAlphabet.charToGsm('\u00e0')); // @@ -97,7 +97,7 @@ public class GsmAlphabetTest extends TestCase { assertEquals('@', GsmAlphabet.gsmToChar(0)); - // `a (a with grave accent) maps to last GSM charater + // `a (a with grave accent) maps to last GSM character assertEquals('\u00e0', GsmAlphabet.gsmToChar(0x7f)); assertEquals('\uffff', @@ -116,8 +116,12 @@ public class GsmAlphabetTest extends TestCase { assertEquals(' ', GsmAlphabet.gsmExtendedToChar( GsmAlphabet.GSM_EXTENDED_ESCAPE)); - // Unmappable - assertEquals(' ', GsmAlphabet.gsmExtendedToChar(0)); + // Reserved for extension to extension table (mapped to space) + assertEquals(' ', GsmAlphabet.gsmExtendedToChar(GsmAlphabet.GSM_EXTENDED_ESCAPE)); + + // Unmappable (mapped to character in default or national locking shift table) + assertEquals('@', GsmAlphabet.gsmExtendedToChar(0)); + assertEquals('\u00e0', GsmAlphabet.gsmExtendedToChar(0x7f)); // // stringTo7BitPacked, gsm7BitPackedToString @@ -128,7 +132,7 @@ public class GsmAlphabetTest extends TestCase { // Check all alignment cases for (int i = 0; i < 9; i++, testString.append('@')) { - packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString()); + packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); assertEquals(testString.toString(), GsmAlphabet.gsm7BitPackedToString(packed, 1, 0xff & packed[0])); } @@ -149,7 +153,7 @@ public class GsmAlphabetTest extends TestCase { assertEquals(1, GsmAlphabet.countGsmSeptets(c)); } - packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString()); + packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); assertEquals(testString.toString(), GsmAlphabet.gsm7BitPackedToString(packed, 1, 0xff & packed[0])); @@ -164,7 +168,7 @@ public class GsmAlphabetTest extends TestCase { } - packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString()); + packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); assertEquals(testString.toString(), GsmAlphabet.gsm7BitPackedToString(packed, 1, 0xff & packed[0])); @@ -175,7 +179,7 @@ public class GsmAlphabetTest extends TestCase { testString.append('@'); } - packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString()); + packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); assertEquals(testString.toString(), GsmAlphabet.gsm7BitPackedToString(packed, 1, 0xff & packed[0])); @@ -183,7 +187,7 @@ public class GsmAlphabetTest extends TestCase { testString.append('@'); try { - GsmAlphabet.stringToGsm7BitPacked(testString.toString()); + GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); fail("expected exception"); } catch (EncodeException ex) { // exception expected @@ -196,7 +200,7 @@ public class GsmAlphabetTest extends TestCase { testString.append('{'); } - packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString()); + packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); assertEquals(testString.toString(), GsmAlphabet.gsm7BitPackedToString(packed, 1, 0xff & packed[0])); @@ -204,17 +208,29 @@ public class GsmAlphabetTest extends TestCase { testString.append('{'); try { - GsmAlphabet.stringToGsm7BitPacked(testString.toString()); + GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); fail("expected exception"); } catch (EncodeException ex) { // exception expected } + // Reserved for extension to extension table (mapped to space) + packed = new byte[]{(byte)(0x1b | 0x80), 0x1b >> 1}; + assertEquals(" ", GsmAlphabet.gsm7BitPackedToString(packed, 0, 2)); + + // Unmappable (mapped to character in default alphabet table) + packed[0] = 0x1b; + packed[1] = 0x00; + assertEquals("@", GsmAlphabet.gsm7BitPackedToString(packed, 0, 2)); + packed[0] = (byte)(0x1b | 0x80); + packed[1] = (byte)(0x7f >> 1); + assertEquals("\u00e0", GsmAlphabet.gsm7BitPackedToString(packed, 0, 2)); + // // 8 bit unpacked format // // Note: we compare hex strings here - // because Assert doesnt have array-comparisons + // because Assert doesn't have array comparisons byte unpacked[]; @@ -306,5 +322,16 @@ public class GsmAlphabetTest extends TestCase { assertEquals("a", GsmAlphabet.gsm8BitUnpackedToString(unpacked, 1, unpacked.length - 1)); + + // Reserved for extension to extension table (mapped to space) + unpacked[0] = 0x1b; + unpacked[1] = 0x1b; + assertEquals(" ", GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, 2)); + + // Unmappable (mapped to character in default or national locking shift table) + unpacked[1] = 0x00; + assertEquals("@", GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, 2)); + unpacked[1] = 0x7f; + assertEquals("\u00e0", GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, 2)); } } diff --git a/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmSmsTest.java b/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmSmsTest.java index 3103fc1..41a719e 100644 --- a/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmSmsTest.java +++ b/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmSmsTest.java @@ -16,16 +16,12 @@ package com.android.internal.telephony; -import com.android.internal.telephony.GsmAlphabet; -import com.android.internal.telephony.SmsHeader; import com.android.internal.telephony.gsm.SmsMessage; import com.android.internal.util.HexDump; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; -import android.util.Log; - public class GsmSmsTest extends AndroidTestCase { @SmallTest @@ -211,8 +207,38 @@ public class GsmSmsTest extends AndroidTestCase { sms.getMessageBody()); } + // GSM 7 bit tables in String form, Escape (0x1B) replaced with '@' + private static final String[] sBasicTables = { + // GSM 7 bit default alphabet + "@\u00a3$\u00a5\u00e8\u00e9\u00f9\u00ec\u00f2\u00c7\n\u00d8\u00f8\r\u00c5\u00e5\u0394_" + + "\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e@\u00c6\u00e6\u00df\u00c9" + + " !\"#\u00a4%&'()*+,-./0123456789:;<=>?\u00a1ABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c4\u00d6" + + "\u00d1\u00dc\u00a7\u00bfabcdefghijklmnopqrstuvwxyz\u00e4\u00f6\u00f1\u00fc\u00e0", + + // Turkish locking shift table + "@\u00a3$\u00a5\u20ac\u00e9\u00f9\u0131\u00f2\u00c7\n\u011e\u011f\r\u00c5\u00e5\u0394_" + + "\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e@\u015e\u015f\u00df\u00c9" + + " !\"#\u00a4%&'()*+,-./0123456789:;<=>?\u0130ABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c4\u00d6" + + "\u00d1\u00dc\u00a7\u00e7abcdefghijklmnopqrstuvwxyz\u00e4\u00f6\u00f1\u00fc\u00e0", + + // no locking shift table defined for Spanish + "", + + // Portuguese locking shift table + "@\u00a3$\u00a5\u00ea\u00e9\u00fa\u00ed\u00f3\u00e7\n\u00d4\u00f4\r\u00c1\u00e1\u0394_" + + "\u00aa\u00c7\u00c0\u221e^\\\u20ac\u00d3|@\u00c2\u00e2\u00ca\u00c9 !\"#\u00ba%&'()" + + "*+,-./0123456789:;<=>?\u00cdABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c3\u00d5\u00da\u00dc" + + "\u00a7~abcdefghijklmnopqrstuvwxyz\u00e3\u00f5`\u00fc\u00e0" + }; + @SmallTest public void testDecode() throws Exception { + decodeSingle(0); // default table + decodeSingle(1); // Turkish locking shift table + decodeSingle(3); // Portuguese locking shift table + } + + private void decodeSingle(int language) throws Exception { byte[] septets = new byte[(7 * 128 + 7) / 8]; int bitOffset = 0; @@ -238,15 +264,168 @@ public class GsmSmsTest extends AndroidTestCase { bitOffset += 7; } - String decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, 128); - byte[] reEncoded = GsmAlphabet.stringToGsm7BitPacked(decoded); + String decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, 128, 0, language, 0); + byte[] reEncoded = GsmAlphabet.stringToGsm7BitPacked(decoded, language, 0); + + assertEquals(sBasicTables[language], decoded); // reEncoded has the count septets byte at the front - assertEquals(reEncoded.length, septets.length + 1); + assertEquals(septets.length + 1, reEncoded.length); for (int i = 0; i < septets.length; i++) { - assertEquals(reEncoded[i + 1], septets[i]); + assertEquals(septets[i], reEncoded[i + 1]); + } + } + + private static final int GSM_ESCAPE_CHARACTER = 0x1b; + + private static final String[] sExtendedTables = { + // GSM 7 bit default alphabet extension table + "\f^{}\\[~]|\u20ac", + + // Turkish single shift extension table + "\f^{}\\[~]|\u011e\u0130\u015e\u00e7\u20ac\u011f\u0131\u015f", + + // Spanish single shift extension table + "\u00e7\f^{}\\[~]|\u00c1\u00cd\u00d3\u00da\u00e1\u20ac\u00ed\u00f3\u00fa", + + // Portuguese single shift extension table + "\u00ea\u00e7\f\u00d4\u00f4\u00c1\u00e1\u03a6\u0393^\u03a9\u03a0\u03a8\u03a3\u0398\u00ca" + + "{}\\[~]|\u00c0\u00cd\u00d3\u00da\u00c3\u00d5\u00c2\u20ac\u00ed\u00f3\u00fa\u00e3" + + "\u00f5\u00e2" + }; + + private static final int[][] sExtendedTableIndexes = { + {0x0a, 0x14, 0x28, 0x29, 0x2f, 0x3c, 0x3d, 0x3e, 0x40, 0x65}, + {0x0a, 0x14, 0x28, 0x29, 0x2f, 0x3c, 0x3d, 0x3e, 0x40, 0x47, 0x49, 0x53, 0x63, + 0x65, 0x67, 0x69, 0x73}, + {0x09, 0x0a, 0x14, 0x28, 0x29, 0x2f, 0x3c, 0x3d, 0x3e, 0x40, 0x41, 0x49, 0x4f, + 0x55, 0x61, 0x65, 0x69, 0x6f, 0x75}, + {0x05, 0x09, 0x0a, 0x0b, 0x0c, 0x0e, 0x0f, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1f, 0x28, 0x29, 0x2f, 0x3c, 0x3d, 0x3e, 0x40, 0x41, 0x49, + 0x4f, 0x55, 0x5b, 0x5c, 0x61, 0x65, 0x69, 0x6f, 0x75, 0x7b, 0x7c, 0x7f} + }; + + @SmallTest + public void testDecodeExtended() throws Exception { + for (int language = 0; language < 3; language++) { + int[] tableIndex = sExtendedTableIndexes[language]; + int numSeptets = tableIndex.length * 2; // two septets per extended char + byte[] septets = new byte[(7 * numSeptets + 7) / 8]; + + int bitOffset = 0; + + for (int v : tableIndex) { + // escape character + int byteOffset = bitOffset / 8; + int shift = bitOffset % 8; + + septets[byteOffset] |= GSM_ESCAPE_CHARACTER << shift; + + if (shift > 1) { + septets[byteOffset + 1] = (byte) (GSM_ESCAPE_CHARACTER >> (8 - shift)); + } + + bitOffset += 7; + + // extended table index + byteOffset = bitOffset / 8; + shift = bitOffset % 8; + + septets[byteOffset] |= v << shift; + + if (shift > 1) { + septets[byteOffset + 1] = (byte) (v >> (8 - shift)); + } + + bitOffset += 7; + } + + String decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, numSeptets, 0, + 0, language); + byte[] reEncoded = GsmAlphabet.stringToGsm7BitPacked(decoded, 0, language); + + assertEquals(sExtendedTables[language], decoded); + + // reEncoded has the count septets byte at the front + assertEquals(septets.length + 1, reEncoded.length); + + for (int i = 0; i < septets.length; i++) { + assertEquals(septets[i], reEncoded[i + 1]); + } } } + @SmallTest + public void testDecodeExtendedFallback() throws Exception { + // verify that unmapped characters in extension table fall back to locking shift table + for (int language = 0; language < 3; language++) { + int[] tableIndex = sExtendedTableIndexes[language]; + int numChars = 128 - tableIndex.length; + int numSeptets = numChars * 2; // two septets per extended char + byte[] septets = new byte[(7 * numSeptets + 7) / 8]; + + int tableOffset = 0; + int bitOffset = 0; + + StringBuilder defaultTable = new StringBuilder(128); + StringBuilder turkishTable = new StringBuilder(128); + StringBuilder portugueseTable = new StringBuilder(128); + + for (char c = 0; c < 128; c++) { + // skip characters that are present in the current extension table + if (tableOffset < tableIndex.length && tableIndex[tableOffset] == c) { + tableOffset++; + continue; + } + + // escape character + int byteOffset = bitOffset / 8; + int shift = bitOffset % 8; + + septets[byteOffset] |= GSM_ESCAPE_CHARACTER << shift; + + if (shift > 1) { + septets[byteOffset + 1] = (byte) (GSM_ESCAPE_CHARACTER >> (8 - shift)); + } + + bitOffset += 7; + + // extended table index + byteOffset = bitOffset / 8; + shift = bitOffset % 8; + + septets[byteOffset] |= c << shift; + + if (shift > 1) { + septets[byteOffset + 1] = (byte) (c >> (8 - shift)); + } + + bitOffset += 7; + + if (c == GsmAlphabet.GSM_EXTENDED_ESCAPE) { + // double Escape maps to space character + defaultTable.append(' '); + turkishTable.append(' '); + portugueseTable.append(' '); + } else { + // other unmapped chars map to the default or locking shift table + defaultTable.append(sBasicTables[0].charAt(c)); + turkishTable.append(sBasicTables[1].charAt(c)); + portugueseTable.append(sBasicTables[3].charAt(c)); + } + } + + String decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, numSeptets, 0, + 0, language); + + assertEquals(defaultTable.toString(), decoded); + + decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, numSeptets, 0, 1, language); + assertEquals(turkishTable.toString(), decoded); + + decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, numSeptets, 0, 3, language); + assertEquals(portugueseTable.toString(), decoded); + } + } } diff --git a/telephony/tests/telephonytests/src/com/android/internal/telephony/SmsMessageBodyTest.java b/telephony/tests/telephonytests/src/com/android/internal/telephony/SmsMessageBodyTest.java index b214887..170bd9b 100644 --- a/telephony/tests/telephonytests/src/com/android/internal/telephony/SmsMessageBodyTest.java +++ b/telephony/tests/telephonytests/src/com/android/internal/telephony/SmsMessageBodyTest.java @@ -19,21 +19,57 @@ package com.android.internal.telephony; import android.telephony.SmsMessage; import android.telephony.TelephonyManager; import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; +import java.util.Random; + +import static android.telephony.SmsMessage.MAX_USER_DATA_BYTES; +import static android.telephony.SmsMessage.MAX_USER_DATA_BYTES_WITH_HEADER; import static android.telephony.SmsMessage.MAX_USER_DATA_SEPTETS; +import static android.telephony.SmsMessage.MAX_USER_DATA_SEPTETS_WITH_HEADER; +/** + * Test cases to verify selection of the optimal 7 bit encoding tables + * (for all combinations of enabled national language tables) for messages + * containing Turkish, Spanish, Portuguese, Greek, and other symbols + * present in the GSM default and national language tables defined in + * 3GPP TS 23.038. Also verifies correct SMS encoding for CDMA, which only + * supports the GSM 7 bit default alphabet, ASCII 8 bit, and UCS-2. + * Tests both encoding variations: unsupported characters mapped to space, + * and unsupported characters force entire message to UCS-2. + */ public class SmsMessageBodyTest extends AndroidTestCase { + private static final String TAG = "SmsMessageBodyTest"; + // ASCII chars in the GSM 7 bit default alphabet private static final String sAsciiChars = "@$_ !\"#%&'()*+,-./0123456789" + ":;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n\r"; - private static final String sGsmBasicChars = "\u00a3\u00a5\u00e8\u00e9" + - "\u00f9\u00ec\u00f2\u00c7\u00d8\u00f8\u00c5\u00e5\u0394\u03a6" + - "\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u00c6\u00e6" + - "\u00df\u00c9\u00a4\u00a1\u00c4\u00d6\u00d1\u00dc\u00a7\u00bf" + - "\u00e4\u00f6\u00f1\u00fc\u00e0"; - private static final String sGsmExtendedAsciiChars = "{|}\\[~]^\f"; + + // Unicode chars in the GSM 7 bit default alphabet and both locking shift tables + private static final String sGsmDefaultChars = "\u00a3\u00a5\u00e9\u00c7\u0394\u00c9" + + "\u00dc\u00a7\u00fc\u00e0"; + + // Unicode chars in the GSM 7 bit default table and Turkish locking shift tables + private static final String sGsmDefaultAndTurkishTables = "\u00f9\u00f2\u00c5\u00e5\u00df" + + "\u00a4\u00c4\u00d6\u00d1\u00e4\u00f6\u00f1"; + + // Unicode chars in the GSM 7 bit default table but not the locking shift tables + private static final String sGsmDefaultTableOnly = "\u00e8\u00ec\u00d8\u00f8\u00c6\u00e6" + + "\u00a1\u00bf"; + + // ASCII chars in the GSM default extension table + private static final String sGsmExtendedAsciiChars = "{}[]\f"; + + // chars in GSM default extension table and Portuguese locking shift table + private static final String sGsmExtendedPortugueseLocking = "^\\|~"; + + // Euro currency symbol private static final String sGsmExtendedEuroSymbol = "\u20ac"; + + // CJK ideographs, Hiragana, Katakana, full width letters, Cyrillic, etc. private static final String sUnicodeChars = "\u4e00\u4e01\u4e02\u4e03" + "\u4e04\u4e05\u4e06\u4e07\u4e08\u4e09\u4e0a\u4e0b\u4e0c\u4e0d" + "\u4e0e\u4e0f\u3041\u3042\u3043\u3044\u3045\u3046\u3047\u3048" + @@ -43,6 +79,86 @@ public class SmsMessageBodyTest extends AndroidTestCase { "\u0400\u0401\u0402\u0403\u0404\u0405\u0406\u0407\u0408" + "\u00a2\u00a9\u00ae\u2122"; + // chars in Turkish single shift and locking shift tables + private static final String sTurkishChars = "\u0131\u011e\u011f\u015e\u015f\u0130"; + + // chars in Spanish single shift table and Portuguese single and locking shift tables + private static final String sPortugueseAndSpanishChars = "\u00c1\u00e1\u00cd\u00ed" + + "\u00d3\u00f3\u00da\u00fa"; + + // chars in all national language tables but not in the standard GSM alphabets + private static final String sNationalLanguageTablesOnly = "\u00e7"; + + // chars in Portuguese single shift and locking shift tables + private static final String sPortugueseChars = "\u00ea\u00d4\u00f4\u00c0\u00c2\u00e2" + + "\u00ca\u00c3\u00d5\u00e3\u00f5"; + + // chars in Portuguese locking shift table only + private static final String sPortugueseLockingShiftChars = "\u00aa\u221e\u00ba`"; + + // Greek letters in GSM alphabet missing from Portuguese locking and single shift tables + private static final String sGreekLettersNotInPortugueseTables = "\u039b\u039e"; + + // Greek letters in GSM alphabet and Portuguese single shift (but not locking shift) table + private static final String sGreekLettersInPortugueseShiftTable = + "\u03a6\u0393\u03a9\u03a0\u03a8\u03a3\u0398"; + + // List of classes of characters in SMS tables + private static final String[] sCharacterClasses = { + sGsmExtendedAsciiChars, + sGsmExtendedPortugueseLocking, + sGsmDefaultChars, + sGsmDefaultAndTurkishTables, + sGsmDefaultTableOnly, + sGsmExtendedEuroSymbol, + sUnicodeChars, + sTurkishChars, + sPortugueseChars, + sPortugueseLockingShiftChars, + sPortugueseAndSpanishChars, + sGreekLettersNotInPortugueseTables, + sGreekLettersInPortugueseShiftTable, + sNationalLanguageTablesOnly, + sAsciiChars + }; + + private static final int sNumCharacterClasses = sCharacterClasses.length; + + // For each character class, whether it is present in a particular char table. + // First three entries are locking shift tables, followed by four single shift tables + private static final boolean[][] sCharClassPresenceInTables = { + // ASCII chars in all GSM extension tables + {false, false, false, true, true, true, true}, + // ASCII chars in all GSM extension tables and Portuguese locking shift table + {false, false, true, true, true, true, true}, + // non-ASCII chars in GSM default alphabet and all locking tables + {true, true, true, false, false, false, false}, + // non-ASCII chars in GSM default alphabet and Turkish locking shift table + {true, true, false, false, false, false, false}, + // non-ASCII chars in GSM default alphabet table only + {true, false, false, false, false, false, false}, + // Euro symbol is present in several tables + {false, true, true, true, true, true, true}, + // Unicode characters not present in any 7 bit tables + {false, false, false, false, false, false, false}, + // Characters specific to Turkish language + {false, true, false, false, true, false, false}, + // Characters in Portuguese single shift and locking shift tables + {false, false, true, false, false, false, true}, + // Characters in Portuguese locking shift table only + {false, false, true, false, false, false, false}, + // Chars in Spanish single shift and Portuguese single and locking shift tables + {false, false, true, false, false, true, true}, + // Greek letters in GSM default alphabet missing from Portuguese tables + {true, true, false, false, false, false, false}, + // Greek letters in GSM alphabet and Portuguese single shift table + {true, true, false, false, false, false, true}, + // Chars in all national language tables but not the standard GSM tables + {false, true, true, false, true, true, true}, + // ASCII chars in GSM default alphabet + {true, true, true, false, false, false, false} + }; + private static final int sTestLengthCount = 12; private static final int[] sSeptetTestLengths = @@ -60,11 +176,92 @@ public class SmsMessageBodyTest extends AndroidTestCase { private static final int[] sUnicodeUnitsRemaining = { 70, 69, 68, 35, 1, 0, 63, 34, 1, 0, 66, 41}; + // Combinations of enabled GSM national language single shift tables + private static final int[][] sEnabledSingleShiftTables = { + {}, // GSM default alphabet only + {1}, // Turkish (single shift only) + {1}, // Turkish (single and locking shift) + {2}, // Spanish + {3}, // Portuguese (single shift only) + {3}, // Portuguese (single and locking shift) + {1, 2}, // Turkish + Spanish (single shift only) + {1, 2}, // Turkish + Spanish (single and locking shift) + {1, 3}, // Turkish + Portuguese (single shift only) + {1, 3}, // Turkish + Portuguese (single and locking shift) + {2, 3}, // Spanish + Portuguese (single shift only) + {2, 3}, // Spanish + Portuguese (single and locking shift) + {1, 2, 3}, // Turkish, Spanish, Portuguese (single shift only) + {1, 2, 3}, // Turkish, Spanish, Portuguese (single and locking shift) + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} // all language tables + }; + + // Combinations of enabled GSM national language locking shift tables + private static final int[][] sEnabledLockingShiftTables = { + {}, // GSM default alphabet only + {}, // Turkish (single shift only) + {1}, // Turkish (single and locking shift) + {}, // Spanish (no locking shift table) + {}, // Portuguese (single shift only) + {3}, // Portuguese (single and locking shift) + {}, // Turkish + Spanish (single shift only) + {1}, // Turkish + Spanish (single and locking shift) + {}, // Turkish + Portuguese (single shift only) + {1, 3}, // Turkish + Portuguese (single and locking shift) + {}, // Spanish + Portuguese (single shift only) + {3}, // Spanish + Portuguese (single and locking shift) + {}, // Turkish, Spanish, Portuguese (single shift only) + {1, 3}, // Turkish, Spanish, Portuguese (single and locking shift) + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} // all language tables + }; + + // LanguagePair counter indexes to check for each entry above + private static final int[][] sLanguagePairIndexesByEnabledIndex = { + {0}, // default tables only + {0, 1}, // Turkish (single shift only) + {0, 1, 4, 5}, // Turkish (single and locking shift) + {0, 2}, // Spanish + {0, 3}, // Portuguese (single shift only) + {0, 3, 8, 11}, // Portuguese (single and locking shift) + {0, 1, 2}, // Turkish + Spanish (single shift only) + {0, 1, 2, 4, 5, 6}, // Turkish + Spanish (single and locking shift) + {0, 1, 3}, // Turkish + Portuguese (single shift only) + {0, 1, 3, 4, 5, 7, 8, 9, 11}, // Turkish + Portuguese (single and locking shift) + {0, 2, 3}, // Spanish + Portuguese (single shift only) + {0, 2, 3, 8, 10, 11}, // Spanish + Portuguese (single and locking shift) + {0, 1, 2, 3}, // all languages (single shift only) + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, // all languages (single and locking shift) + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} // all languages (no Indic chars in test) + }; + + /** + * User data header requires one octet for length. Count as one septet, because + * all combinations of header elements below will have at least one free bit + * when padding to the nearest septet boundary. + */ + private static final int UDH_SEPTET_COST_LENGTH = 1; + + /** + * Using a non-default language locking shift table OR single shift table + * requires a user data header of 3 octets, or 4 septets, plus UDH length. + */ + private static final int UDH_SEPTET_COST_ONE_SHIFT_TABLE = 4; + + /** + * Using a non-default language locking shift table AND single shift table + * requires a user data header of 6 octets, or 7 septets, plus UDH length. + */ + private static final int UDH_SEPTET_COST_TWO_SHIFT_TABLES = 7; + + /** + * Multi-part messages require a user data header of 5 octets, or 6 septets, + * plus UDH length. + */ + private static final int UDH_SEPTET_COST_CONCATENATED_MESSAGE = 6; @SmallTest public void testCalcLengthAscii() throws Exception { StringBuilder sb = new StringBuilder(320); - int[] values = {0, 0, 0, SmsMessage.ENCODING_7BIT}; + int[] values = {0, 0, 0, SmsMessage.ENCODING_7BIT, 0, 0}; int startPos = 0; int asciiCharsLen = sAsciiChars.length(); @@ -94,20 +291,10 @@ public class SmsMessageBodyTest extends AndroidTestCase { } @SmallTest - public void testCalcLength7bitGsm() throws Exception { - // TODO - } - - @SmallTest - public void testCalcLength7bitGsmExtended() throws Exception { - // TODO - } - - @SmallTest public void testCalcLengthUnicode() throws Exception { StringBuilder sb = new StringBuilder(160); - int[] values = {0, 0, 0, SmsMessage.ENCODING_16BIT}; - int[] values7bit = {1, 0, 0, SmsMessage.ENCODING_7BIT}; + int[] values = {0, 0, 0, SmsMessage.ENCODING_16BIT, 0, 0}; + int[] values7bit = {1, 0, 0, SmsMessage.ENCODING_7BIT, 0, 0}; int startPos = 0; int unicodeCharsLen = sUnicodeChars.length(); @@ -139,6 +326,229 @@ public class SmsMessageBodyTest extends AndroidTestCase { } } + private static class LanguagePair { + // index is 2 for Portuguese locking shift because there is no Spanish locking shift table + private final int langTableIndex; + private final int langShiftTableIndex; + int length; + int missingChars7bit; + + LanguagePair(int langTable, int langShiftTable) { + langTableIndex = langTable; + langShiftTableIndex = langShiftTable; + } + + void clear() { + length = 0; + missingChars7bit = 0; + } + + void addChar(boolean[] charClassTableRow) { + if (charClassTableRow[langTableIndex]) { + length++; + } else if (charClassTableRow[3 + langShiftTableIndex]) { + length += 2; + } else { + length++; // use ' ' for unmapped char in 7 bit only mode + missingChars7bit++; + } + } + } + + private static class CounterHelper { + LanguagePair[] mCounters; + int[] mStatsCounters; + int mUnicodeCounter; + + CounterHelper() { + mCounters = new LanguagePair[12]; + mStatsCounters = new int[12]; + for (int i = 0; i < 12; i++) { + mCounters[i] = new LanguagePair(i/4, i%4); + } + } + + void clear() { + // Note: don't clear stats counters + for (int i = 0; i < 12; i++) { + mCounters[i].clear(); + } + } + + void addChar(int charClass) { + boolean[] charClassTableRow = sCharClassPresenceInTables[charClass]; + for (int i = 0; i < 12; i++) { + mCounters[i].addChar(charClassTableRow); + } + } + + void fillData(int enabledLangsIndex, boolean use7bitOnly, int[] values, int length) { + int[] languagePairs = sLanguagePairIndexesByEnabledIndex[enabledLangsIndex]; + int minNumSeptets = Integer.MAX_VALUE; + int minNumSeptetsWithHeader = Integer.MAX_VALUE; + int minNumMissingChars = Integer.MAX_VALUE; + int langIndex = -1; + int langShiftIndex = -1; + for (int i : languagePairs) { + LanguagePair pair = mCounters[i]; + int udhLength = 0; + if (i != 0) { + udhLength = UDH_SEPTET_COST_LENGTH; + if (i < 4 || i % 4 == 0) { + udhLength += UDH_SEPTET_COST_ONE_SHIFT_TABLE; + } else { + udhLength += UDH_SEPTET_COST_TWO_SHIFT_TABLES; + } + } + int numSeptetsWithHeader; + if (pair.length > (MAX_USER_DATA_SEPTETS - udhLength)) { + if (udhLength == 0) { + udhLength = UDH_SEPTET_COST_LENGTH; + } + udhLength += UDH_SEPTET_COST_CONCATENATED_MESSAGE; + int septetsPerPart = MAX_USER_DATA_SEPTETS - udhLength; + int msgCount = (pair.length + septetsPerPart - 1) / septetsPerPart; + numSeptetsWithHeader = udhLength * msgCount + pair.length; + } else { + numSeptetsWithHeader = udhLength + pair.length; + } + + if (use7bitOnly) { + if (pair.missingChars7bit < minNumMissingChars || (pair.missingChars7bit == + minNumMissingChars && numSeptetsWithHeader < minNumSeptetsWithHeader)) { + minNumSeptets = pair.length; + minNumSeptetsWithHeader = numSeptetsWithHeader; + minNumMissingChars = pair.missingChars7bit; + langIndex = pair.langTableIndex; + langShiftIndex = pair.langShiftTableIndex; + } + } else { + if (pair.missingChars7bit == 0 && numSeptetsWithHeader < minNumSeptetsWithHeader) { + minNumSeptets = pair.length; + minNumSeptetsWithHeader = numSeptetsWithHeader; + langIndex = pair.langTableIndex; + langShiftIndex = pair.langShiftTableIndex; + } + } + } + if (langIndex == -1) { + // nothing matches, use values for Unicode + int byteCount = length * 2; + if (byteCount > MAX_USER_DATA_BYTES) { + values[0] = (byteCount + MAX_USER_DATA_BYTES_WITH_HEADER - 1) / + MAX_USER_DATA_BYTES_WITH_HEADER; + values[2] = ((values[0] * MAX_USER_DATA_BYTES_WITH_HEADER) - byteCount) / 2; + } else { + values[0] = 1; + values[2] = (MAX_USER_DATA_BYTES - byteCount) / 2; + } + values[1] = length; + values[3] = SmsMessage.ENCODING_16BIT; + values[4] = 0; + values[5] = 0; + mUnicodeCounter++; + } else { + int udhLength = 0; + if (langIndex != 0 || langShiftIndex != 0) { + udhLength = UDH_SEPTET_COST_LENGTH; + if (langIndex == 0 || langShiftIndex == 0) { + udhLength += UDH_SEPTET_COST_ONE_SHIFT_TABLE; + } else { + udhLength += UDH_SEPTET_COST_TWO_SHIFT_TABLES; + } + } + int msgCount; + if (minNumSeptets > (MAX_USER_DATA_SEPTETS - udhLength)) { + if (udhLength == 0) { + udhLength = UDH_SEPTET_COST_LENGTH; + } + udhLength += UDH_SEPTET_COST_CONCATENATED_MESSAGE; + int septetsPerPart = MAX_USER_DATA_SEPTETS - udhLength; + msgCount = (minNumSeptets + septetsPerPart - 1) / septetsPerPart; + } else { + msgCount = 1; + } + values[0] = msgCount; + values[1] = minNumSeptets; + values[2] = (values[0] * (MAX_USER_DATA_SEPTETS - udhLength)) - minNumSeptets; + values[3] = SmsMessage.ENCODING_7BIT; + values[4] = (langIndex == 2 ? 3 : langIndex); // Portuguese is code 3, index 2 + values[5] = langShiftIndex; + assertEquals("minNumSeptetsWithHeader", minNumSeptetsWithHeader, + udhLength * msgCount + minNumSeptets); + mStatsCounters[langIndex * 4 + langShiftIndex]++; + } + } + + void printStats() { + Log.d(TAG, "Unicode selection count: " + mUnicodeCounter); + for (int i = 0; i < 12; i++) { + Log.d(TAG, "Language pair index " + i + " count: " + mStatsCounters[i]); + } + } + } + + @LargeTest + public void testCalcLengthMixed7bit() throws Exception { + StringBuilder sb = new StringBuilder(320); + CounterHelper ch = new CounterHelper(); + Random r = new Random(0x4321); // use the same seed for reproducibility + int[] expectedValues = new int[6]; + int[] origLockingShiftTables = GsmAlphabet.getEnabledLockingShiftTables(); + int[] origSingleShiftTables = GsmAlphabet.getEnabledSingleShiftTables(); + int enabledLanguagesTestCases = sEnabledSingleShiftTables.length; + long startTime = System.currentTimeMillis(); + + // Repeat for 10 test runs + for (int run = 0; run < 10; run++) { + sb.setLength(0); + ch.clear(); + int unicodeOnlyCount = 0; + + // Test incrementally from 1 to 320 character random messages + for (int i = 1; i < 320; i++) { + // 1% chance to add from each special character class, else add an ASCII char + int charClass = r.nextInt(100); + if (charClass >= sNumCharacterClasses) { + charClass = sNumCharacterClasses - 1; // last class is ASCII + } + int classLength = sCharacterClasses[charClass].length(); + char nextChar = sCharacterClasses[charClass].charAt(r.nextInt(classLength)); + sb.append(nextChar); + ch.addChar(charClass); + +// if (i % 20 == 0) { +// Log.d(TAG, "test string: " + sb); +// } + + // Test string against all combinations of enabled languages + boolean unicodeOnly = true; + for (int j = 0; j < enabledLanguagesTestCases; j++) { + GsmAlphabet.setEnabledSingleShiftTables(sEnabledSingleShiftTables[j]); + GsmAlphabet.setEnabledLockingShiftTables(sEnabledLockingShiftTables[j]); + ch.fillData(j, false, expectedValues, i); + if (expectedValues[3] == SmsMessage.ENCODING_7BIT) { + unicodeOnly = false; + } + callGsmLengthMethods(sb, false, expectedValues); + // test 7 bit only mode + ch.fillData(j, true, expectedValues, i); + callGsmLengthMethods(sb, true, expectedValues); + } + // after 10 iterations with a Unicode-only string, skip to next test string + // so we can spend more time testing strings that do encode into 7 bits. + if (unicodeOnly && ++unicodeOnlyCount == 10) { +// Log.d(TAG, "Unicode only: skipping to next test string"); + break; + } + } + } + ch.printStats(); + Log.d(TAG, "Completed in " + (System.currentTimeMillis() - startTime) + " ms"); + GsmAlphabet.setEnabledLockingShiftTables(origLockingShiftTables); + GsmAlphabet.setEnabledSingleShiftTables(origSingleShiftTables); + } + private void callGsmLengthMethods(CharSequence msgBody, boolean use7bitOnly, int[] expectedValues) { @@ -164,6 +574,8 @@ public class SmsMessageBodyTest extends AndroidTestCase { assertEquals("codeUnitCount", expectedValues[1], ted.codeUnitCount); assertEquals("codeUnitsRemaining", expectedValues[2], ted.codeUnitsRemaining); assertEquals("codeUnitSize", expectedValues[3], ted.codeUnitSize); + assertEquals("languageTable", expectedValues[4], ted.languageTable); + assertEquals("languageShiftTable", expectedValues[5], ted.languageShiftTable); } private void callCdmaLengthMethods(CharSequence msgBody, boolean use7bitOnly, |