diff --git a/src/com/android/email/mail/Address.java b/src/com/android/email/mail/Address.java index fe75a1454..0d37ac936 100644 --- a/src/com/android/email/mail/Address.java +++ b/src/com/android/email/mail/Address.java @@ -25,8 +25,6 @@ import android.text.TextUtils; import android.text.util.Rfc822Token; import android.text.util.Rfc822Tokenizer; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.util.ArrayList; import java.util.regex.Pattern; @@ -62,6 +60,10 @@ public class Address { private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0]; + // delimiters are chars that do not appear in an email address, used by pack/unpack + private static final char LIST_DELIMITER_EMAIL = '\1'; + private static final char LIST_DELIMITER_PERSONAL = '\2'; + public Address(String address, String personal) { setAddress(address); setPersonal(personal); @@ -292,13 +294,98 @@ public class Address { } return sb.toString(); } - + /** - * Unpacks an address list previously packed with packAddressList() - * @param list - * @return + * Unpacks an address list previously packed with pack() + * @param addressList String with packed addresses as returned by pack() + * @return array of addresses resulting from unpack */ public static Address[] unpack(String addressList) { + if (addressList == null || addressList.length() == 0) { + return EMPTY_ADDRESS_ARRAY; + } + ArrayList
addresses = new ArrayList
(); + int length = addressList.length(); + int pairStartIndex = 0; + int pairEndIndex = 0; + + /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL + is used, not for every email address; i.e. not for every iteration of the while(). + This reduces the theoretical complexity from quadratic to linear, + and provides some speed-up in practice by removing redundant scans of the string. + */ + int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL); + + while (pairStartIndex < length) { + pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex); + if (pairEndIndex == -1) { + pairEndIndex = length; + } + Address address; + if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) { + // in this case the DELIMITER_PERSONAL is in a future pair, + // so don't use personal, and don't update addressEndIndex + address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null); + } else { + address = new Address(addressList.substring(pairStartIndex, addressEndIndex), + addressList.substring(addressEndIndex + 1, pairEndIndex)); + // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL + addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1); + } + addresses.add(address); + pairStartIndex = pairEndIndex + 1; + } + return addresses.toArray(EMPTY_ADDRESS_ARRAY); + } + + /** + * Packs an address list into a String that is very quick to read + * and parse. Packed lists can be unpacked with unpack(). + * The format is a series of packed addresses separated by LIST_DELIMITER_EMAIL. + * Each address is packed as + * a pair of address and personal separated by LIST_DELIMITER_PERSONAL, + * where the personal and delimiter are optional. + * E.g. "foo@x.com\1joe@x.com\2Joe Doe" + * @param addresses Array of addresses + * @return a string containing the packed addresses. + */ + public static String pack(Address[] addresses) { + // TODO: return same value for both null & empty list + if (addresses == null) { + return null; + } + final int nAddr = addresses.length; + if (nAddr == 0) { + return ""; + } + + // shortcut: one email with no displayName + if (nAddr == 1 && addresses[0].getPersonal() == null) { + return addresses[0].getAddress(); + } + + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < nAddr; i++) { + final Address address = addresses[i]; + sb.append(address.getAddress()); + final String displayName = address.getPersonal(); + if (displayName != null) { + sb.append(LIST_DELIMITER_PERSONAL); + sb.append(displayName); + } + if (i < nAddr - 1) { + sb.append(LIST_DELIMITER_EMAIL); + } + } + return sb.toString(); + } + + /** + * Legacy unpack() used for reading the old data (migration), + * as found in LocalStore (Donut; db version up to 24). + * @See unpack() + */ + /* package */ static Address[] legacyUnpack(String addressList) { if (addressList == null || addressList.length() == 0) { return new Address[] { }; } @@ -316,49 +403,18 @@ public class Address { String address = null; String personal = null; if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) { - address = Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex)); + address = + Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex)); } else { - address = Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex)); - personal = Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex)); + address = + Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex)); + personal = + Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex)); } addresses.add(new Address(address, personal)); pairStartIndex = pairEndIndex + 1; } return addresses.toArray(new Address[] { }); } - - /** - * Packs an address list into a String that is very quick to read - * and parse. Packed lists can be unpacked with unpackAddressList() - * The packed list is a comma separated list of: - * URLENCODE(address)[;URLENCODE(personal)] - * @param list - * @return - */ - public static String pack(Address[] addresses) { - if (addresses == null) { - return null; - } else if (addresses.length == 0) { - return ""; - } - StringBuffer sb = new StringBuffer(); - for (int i = 0, count = addresses.length; i < count; i++) { - Address address = addresses[i]; - try { - sb.append(URLEncoder.encode(address.getAddress(), "UTF-8")); - if (address.getPersonal() != null) { - sb.append(';'); - sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8")); - } - if (i < count - 1) { - sb.append(','); - } - } - catch (UnsupportedEncodingException uee) { - return null; - } - } - return sb.toString(); - } } diff --git a/tests/src/com/android/email/mail/AddressUnitTests.java b/tests/src/com/android/email/mail/AddressUnitTests.java index 1feb91c6a..654fff141 100644 --- a/tests/src/com/android/email/mail/AddressUnitTests.java +++ b/tests/src/com/android/email/mail/AddressUnitTests.java @@ -19,6 +19,9 @@ package com.android.email.mail; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; +import java.net.URLEncoder; +import java.io.UnsupportedEncodingException; + /** * This is a series of unit tests for the Address class. These tests must be locally * complete - no server(s) required. @@ -37,6 +40,16 @@ public class AddressUnitTests extends AndroidTestCase { + "\uD834\uDF01\uD834\uDF46 ," + "\"\uD834\uDF01\uD834\uDF46\" "; private static final int MULTI_ADDRESSES_COUNT = 9; + + private static final Address PACK_ADDR_1 = new Address("john@gmail.com", "John Doe"); + private static final Address PACK_ADDR_2 = new Address("foo@bar.com", null); + private static final Address PACK_ADDR_3 = new Address("mar.y+test@gmail.com", "Mar-y, B; B*arr"); + private static final Address[][] PACK_CASES = { + {PACK_ADDR_2}, {PACK_ADDR_1}, + {PACK_ADDR_1, PACK_ADDR_2}, {PACK_ADDR_2, PACK_ADDR_1}, + {PACK_ADDR_1, PACK_ADDR_3}, {PACK_ADDR_2, PACK_ADDR_2}, + {PACK_ADDR_1, PACK_ADDR_2, PACK_ADDR_3}, {PACK_ADDR_3, PACK_ADDR_1, PACK_ADDR_2} + }; Address mAddress1; Address mAddress2; @@ -472,10 +485,6 @@ public class AddressUnitTests extends AndroidTestCase { assertEquals("personal1,address2,address3", Address.toFriendly(list4)); } - /** - * TODO: more in-depth tests for pack() and unpack() - */ - /** * Simple quick checks of empty-input edge conditions for pack() * @@ -512,6 +521,45 @@ public class AddressUnitTests extends AndroidTestCase { assertTrue("unpacking zero-length", result != null && result.length == 0); } + private static boolean addressEquals(Address a1, Address a2) { + if (!a1.equals(a2)) { + return false; + } + final String displayName1 = a1.getPersonal(); + final String displayName2 = a2.getPersonal(); + if (displayName1 == null) { + return displayName2 == null; + } else { + return displayName1.equals(displayName2); + } + } + + private static boolean addressArrayEquals(Address[] array1, Address[] array2) { + if (array1.length != array2.length) { + return false; + } + for (int i = array1.length - 1; i >= 0; --i) { + if (!addressEquals(array1[i], array2[i])) { + return false; + } + } + return true; + } + + public void testPackUnpack() { + for (Address[] list : PACK_CASES) { + String packed = Address.pack(list); + assertTrue(packed, addressArrayEquals(list, Address.unpack(packed))); + } + } + + public void testLegacyPackUnpack() { + for (Address[] list : PACK_CASES) { + String packed = legacyPack(list); + assertTrue(packed, addressArrayEquals(list, Address.legacyUnpack(packed))); + } + } + public void testIsValidAddress() { String notValid[] = {"", "foo", "john@", "x@y", "x@y.", "foo.com"}; String valid[] = {"x@y.z", "john@gmail.com", "a@b.c.d"}; @@ -525,4 +573,36 @@ public class AddressUnitTests extends AndroidTestCase { // isAllValid() must accept empty address list as valid assertTrue("Empty address list is valid", Address.isAllValid("")); } + + /** + * Legacy pack() used for testing legacyUnpack(). + * The packed list is a comma separated list of: + * URLENCODE(address)[;URLENCODE(personal)] + * @See pack() + */ + private static String legacyPack(Address[] addresses) { + if (addresses == null) { + return null; + } else if (addresses.length == 0) { + return ""; + } + StringBuffer sb = new StringBuffer(); + for (int i = 0, count = addresses.length; i < count; i++) { + Address address = addresses[i]; + try { + sb.append(URLEncoder.encode(address.getAddress(), "UTF-8")); + if (address.getPersonal() != null) { + sb.append(';'); + sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8")); + } + if (i < count - 1) { + sb.append(','); + } + } + catch (UnsupportedEncodingException uee) { + return null; + } + } + return sb.toString(); + } }