diff options
author | Daisuke Miyakawa <dmiyakawa@google.com> | 2010-09-02 10:39:00 -0700 |
---|---|---|
committer | Daisuke Miyakawa <dmiyakawa@google.com> | 2010-09-02 10:39:00 -0700 |
commit | 69831d9dc16c1d36739328910e5d7c0fb7d327fe (patch) | |
tree | 20ad5970493d1c85062c4cbcc1de01a7a7378a43 /core/java/android/pim | |
parent | 60264b306453a3043442719b970f2edb3f46f51b (diff) | |
download | frameworks_base-69831d9dc16c1d36739328910e5d7c0fb7d327fe.zip frameworks_base-69831d9dc16c1d36739328910e5d7c0fb7d327fe.tar.gz frameworks_base-69831d9dc16c1d36739328910e5d7c0fb7d327fe.tar.bz2 |
VCard refactoring backport.
Change-Id: Icf265ce7f83c1e2bd5db0c3d9bd4c142afd6db34
Diffstat (limited to 'core/java/android/pim')
22 files changed, 3034 insertions, 1979 deletions
diff --git a/core/java/android/pim/vcard/JapaneseUtils.java b/core/java/android/pim/vcard/JapaneseUtils.java index 875c29e..dcfe980 100644 --- a/core/java/android/pim/vcard/JapaneseUtils.java +++ b/core/java/android/pim/vcard/JapaneseUtils.java @@ -27,7 +27,6 @@ import java.util.Map; new HashMap<Character, String>(); static { - // There's no logical mapping rule in Unicode. Sigh. sHalfWidthMap.put('\u3001', "\uFF64"); sHalfWidthMap.put('\u3002', "\uFF61"); sHalfWidthMap.put('\u300C', "\uFF62"); @@ -366,11 +365,11 @@ import java.util.Map; } /** - * Return half-width version of that character if possible. Return null if not possible + * Returns half-width version of that character if possible. Returns null if not possible * @param ch input character * @return CharSequence object if the mapping for ch exists. Return null otherwise. */ - public static String tryGetHalfWidthText(char ch) { + public static String tryGetHalfWidthText(final char ch) { if (sHalfWidthMap.containsKey(ch)) { return sHalfWidthMap.get(ch); } else { diff --git a/core/java/android/pim/vcard/VCardBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java index d634672..b2007f2 100644 --- a/core/java/android/pim/vcard/VCardBuilder.java +++ b/core/java/android/pim/vcard/VCardBuilder.java @@ -30,11 +30,10 @@ import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import android.util.Base64; import android.util.CharsetUtils; import android.util.Log; -import org.apache.commons.codec.binary.Base64; - import java.io.UnsupportedEncodingException; import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; @@ -47,7 +46,23 @@ import java.util.Map; import java.util.Set; /** - * The class which lets users create their own vCard String. + * <p> + * The class which lets users create their own vCard String. Typical usage is as follows: + * </p> + * <pre class="prettyprint">final VCardBuilder builder = new VCardBuilder(vcardType); + * builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) + * .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) + * .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) + * .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) + * .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) + * .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) + * .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)) + * .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)) + * .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) + * .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) + * .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) + * .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); + * return builder.toString();</pre> */ public class VCardBuilder { private static final String LOG_TAG = "VCardBuilder"; @@ -75,81 +90,129 @@ public class VCardBuilder { private static final String VCARD_WS = " "; private static final String VCARD_PARAM_EQUAL = "="; - private static final String VCARD_PARAM_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; - - private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64"; - private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b"; + private static final String VCARD_PARAM_ENCODING_QP = + "ENCODING=" + VCardConstants.PARAM_ENCODING_QP; + private static final String VCARD_PARAM_ENCODING_BASE64_V21 = + "ENCODING=" + VCardConstants.PARAM_ENCODING_BASE64; + private static final String VCARD_PARAM_ENCODING_BASE64_AS_B = + "ENCODING=" + VCardConstants.PARAM_ENCODING_B; private static final String SHIFT_JIS = "SHIFT_JIS"; - private static final String UTF_8 = "UTF-8"; private final int mVCardType; - private final boolean mIsV30; + private final boolean mIsV30OrV40; private final boolean mIsJapaneseMobilePhone; private final boolean mOnlyOneNoteFieldIsAvailable; private final boolean mIsDoCoMo; private final boolean mShouldUseQuotedPrintable; private final boolean mUsesAndroidProperty; private final boolean mUsesDefactProperty; - private final boolean mUsesUtf8; - private final boolean mUsesShiftJis; private final boolean mAppendTypeParamName; private final boolean mRefrainsQPToNameProperties; private final boolean mNeedsToConvertPhoneticString; private final boolean mShouldAppendCharsetParam; - private final String mCharsetString; + private final String mCharset; private final String mVCardCharsetParameter; private StringBuilder mBuilder; private boolean mEndAppended; public VCardBuilder(final int vcardType) { + // Default charset should be used + this(vcardType, null); + } + + /** + * @param vcardType + * @param charset If null, we use default charset for export. + * @hide + */ + public VCardBuilder(final int vcardType, String charset) { mVCardType = vcardType; - mIsV30 = VCardConfig.isV30(vcardType); + Log.w(LOG_TAG, + "Should not use vCard 4.0 when building vCard. " + + "It is not officially published yet."); + + mIsV30OrV40 = VCardConfig.isVersion30(vcardType) || VCardConfig.isVersion40(vcardType); mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType); mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType); mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType); mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType); mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); - mUsesUtf8 = VCardConfig.usesUtf8(vcardType); - mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType); mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); - mShouldAppendCharsetParam = !(mIsV30 && mUsesUtf8); - - if (mIsDoCoMo) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - mCharsetString = charset; - // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but - // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in - // Android, not shown to the public). - mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; - } else if (mUsesShiftJis) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - mCharsetString = charset; + // vCard 2.1 requires charset. + // vCard 3.0 does not allow it but we found some devices use it to determine + // the exact charset. + // We currently append it only when charset other than UTF_8 is used. + mShouldAppendCharsetParam = + !(VCardConfig.isVersion30(vcardType) && "UTF-8".equalsIgnoreCase(charset)); + + if (VCardConfig.isDoCoMo(vcardType)) { + if (!SHIFT_JIS.equalsIgnoreCase(charset)) { + Log.w(LOG_TAG, + "The charset \"" + charset + "\" is used while " + + SHIFT_JIS + " is needed to be used."); + if (TextUtils.isEmpty(charset)) { + mCharset = SHIFT_JIS; + } else { + try { + charset = CharsetUtils.charsetForVendor(charset).name(); + } catch (UnsupportedCharsetException e) { + Log.i(LOG_TAG, + "Career-specific \"" + charset + "\" was not found (as usual). " + + "Use it as is."); + } + mCharset = charset; + } + } else { + if (mIsDoCoMo) { + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, + "DoCoMo-specific SHIFT_JIS was not found. " + + "Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + } else { + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, + "Career-specific SHIFT_JIS was not found. " + + "Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + } + mCharset = charset; + } mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; } else { - mCharsetString = UTF_8; - mVCardCharsetParameter = "CHARSET=" + UTF_8; + if (TextUtils.isEmpty(charset)) { + Log.i(LOG_TAG, + "Use the charset \"" + VCardConfig.DEFAULT_EXPORT_CHARSET + + "\" for export."); + mCharset = VCardConfig.DEFAULT_EXPORT_CHARSET; + mVCardCharsetParameter = "CHARSET=" + VCardConfig.DEFAULT_EXPORT_CHARSET; + } else { + try { + charset = CharsetUtils.charsetForVendor(charset).name(); + } catch (UnsupportedCharsetException e) { + Log.i(LOG_TAG, + "Career-specific \"" + charset + "\" was not found (as usual). " + + "Use it as is."); + } + mCharset = charset; + mVCardCharsetParameter = "CHARSET=" + charset; + } } clear(); } @@ -158,9 +221,14 @@ public class VCardBuilder { mBuilder = new StringBuilder(); mEndAppended = false; appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD); - if (mIsV30) { + if (VCardConfig.isVersion40(mVCardType)) { + appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V40); + } else if (VCardConfig.isVersion30(mVCardType)) { appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30); } else { + if (!VCardConfig.isVersion21(mVCardType)) { + Log.w(LOG_TAG, "Unknown vCard version detected."); + } appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21); } } @@ -227,18 +295,127 @@ public class VCardBuilder { } /** + * To avoid unnecessary complication in logic, we use this method to construct N, FN + * properties for vCard 4.0. + */ + private VCardBuilder appendNamePropertiesV40(final List<ContentValues> contentValuesList) { + if (mIsDoCoMo || mNeedsToConvertPhoneticString) { + // Ignore all flags that look stale from the view of vCard 4.0 to + // simplify construction algorithm. Actually we don't have any vCard file + // available from real world yet, so we may need to re-enable some of these + // in the future. + Log.w(LOG_TAG, "Invalid flag is used in vCard 4.0 construction. Ignored."); + } + + if (contentValuesList == null || contentValuesList.isEmpty()) { + appendLine(VCardConstants.PROPERTY_FN, ""); + return this; + } + + // We have difficulty here. How can we appropriately handle StructuredName with + // missing parts necessary for displaying while it has suppremental information. + // + // e.g. How to handle non-empty phonetic names with empty structured names? + + final ContentValues contentValues = getPrimaryContentValue(contentValuesList); + String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); + final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); + final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); + final String prefix = contentValues.getAsString(StructuredName.PREFIX); + final String suffix = contentValues.getAsString(StructuredName.SUFFIX); + final String formattedName = contentValues.getAsString(StructuredName.DISPLAY_NAME); + if (TextUtils.isEmpty(familyName) + && TextUtils.isEmpty(givenName) + && TextUtils.isEmpty(middleName) + && TextUtils.isEmpty(prefix) + && TextUtils.isEmpty(suffix)) { + if (TextUtils.isEmpty(formattedName)) { + appendLine(VCardConstants.PROPERTY_FN, ""); + return this; + } + familyName = formattedName; + } + + final String phoneticFamilyName = + contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); + final String phoneticMiddleName = + contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + final String phoneticGivenName = + contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); + final String escapedFamily = escapeCharacters(familyName); + final String escapedGiven = escapeCharacters(givenName); + final String escapedMiddle = escapeCharacters(middleName); + final String escapedPrefix = escapeCharacters(prefix); + final String escapedSuffix = escapeCharacters(suffix); + + mBuilder.append(VCardConstants.PROPERTY_N); + + if (!(TextUtils.isEmpty(phoneticFamilyName) && + TextUtils.isEmpty(phoneticMiddleName) && + TextUtils.isEmpty(phoneticGivenName))) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + final String sortAs = escapeCharacters(phoneticFamilyName) + + ';' + escapeCharacters(phoneticGivenName) + + ';' + escapeCharacters(phoneticMiddleName); + mBuilder.append("SORT-AS=").append( + VCardUtils.toStringAsV40ParamValue(sortAs)); + } + + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(escapedFamily); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(escapedGiven); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(escapedMiddle); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(escapedPrefix); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(escapedSuffix); + mBuilder.append(VCARD_END_OF_LINE); + + if (TextUtils.isEmpty(formattedName)) { + // Note: + // DISPLAY_NAME doesn't exist while some other elements do, which is usually + // weird in Android, as DISPLAY_NAME should (usually) be constructed + // from the others using locale information and its code points. + Log.w(LOG_TAG, "DISPLAY_NAME is empty."); + + final String escaped = escapeCharacters(VCardUtils.constructNameFromElements( + VCardConfig.getNameOrderType(mVCardType), + familyName, middleName, givenName, prefix, suffix)); + appendLine(VCardConstants.PROPERTY_FN, escaped); + } else { + final String escapedFormatted = escapeCharacters(formattedName); + mBuilder.append(VCardConstants.PROPERTY_FN); + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(escapedFormatted); + mBuilder.append(VCARD_END_OF_LINE); + } + + // We may need X- properties for phonetic names. + appendPhoneticNameFields(contentValues); + return this; + } + + /** * For safety, we'll emit just one value around StructuredName, as external importers * may get confused with multiple "N", "FN", etc. properties, though it is valid in * vCard spec. */ public VCardBuilder appendNameProperties(final List<ContentValues> contentValuesList) { + if (VCardConfig.isVersion40(mVCardType)) { + return appendNamePropertiesV40(contentValuesList); + } + if (contentValuesList == null || contentValuesList.isEmpty()) { - if (mIsDoCoMo) { - appendLine(VCardConstants.PROPERTY_N, ""); - } else if (mIsV30) { + if (VCardConfig.isVersion30(mVCardType)) { // vCard 3.0 requires "N" and "FN" properties. + // vCard 4.0 does NOT require N, but we take care of possible backward + // compatibility issues. appendLine(VCardConstants.PROPERTY_N, ""); appendLine(VCardConstants.PROPERTY_FN, ""); + } else if (mIsDoCoMo) { + appendLine(VCardConstants.PROPERTY_N, ""); } return this; } @@ -360,6 +537,7 @@ public class VCardBuilder { encodeQuotedPrintable(displayName) : escapeCharacters(displayName); + // N mBuilder.append(VCardConstants.PROPERTY_N); if (shouldAppendCharsetParam(displayName)) { mBuilder.append(VCARD_PARAM_SEPARATOR); @@ -376,11 +554,13 @@ public class VCardBuilder { mBuilder.append(VCARD_ITEM_SEPARATOR); mBuilder.append(VCARD_ITEM_SEPARATOR); mBuilder.append(VCARD_END_OF_LINE); + + // FN mBuilder.append(VCardConstants.PROPERTY_FN); // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it - // when it would be useful for external importers, assuming no external - // importer allows this vioration. + // when it would be useful or necessary for external importers, + // assuming the external importer allows this vioration of the spec. if (shouldAppendCharsetParam(displayName)) { mBuilder.append(VCARD_PARAM_SEPARATOR); mBuilder.append(mVCardCharsetParameter); @@ -388,8 +568,7 @@ public class VCardBuilder { mBuilder.append(VCARD_DATA_SEPARATOR); mBuilder.append(encodedDisplayName); mBuilder.append(VCARD_END_OF_LINE); - } else if (mIsV30) { - // vCard 3.0 specification requires these fields. + } else if (VCardConfig.isVersion30(mVCardType)) { appendLine(VCardConstants.PROPERTY_N, ""); appendLine(VCardConstants.PROPERTY_FN, ""); } else if (mIsDoCoMo) { @@ -400,6 +579,9 @@ public class VCardBuilder { return this; } + /** + * Emits SOUND;IRMC, SORT-STRING, and de-fact values for phonetic names like X-PHONETIC-FAMILY. + */ private void appendPhoneticNameFields(final ContentValues contentValues) { final String phoneticFamilyName; final String phoneticMiddleName; @@ -439,13 +621,18 @@ public class VCardBuilder { return; } - // Try to emit the field(s) related to phonetic name. - if (mIsV30) { - final String sortString = VCardUtils - .constructNameFromElements(mVCardType, + if (VCardConfig.isVersion40(mVCardType)) { + // We don't want SORT-STRING anyway. + } else if (VCardConfig.isVersion30(mVCardType)) { + final String sortString = + VCardUtils.constructNameFromElements(mVCardType, phoneticFamilyName, phoneticMiddleName, phoneticGivenName); mBuilder.append(VCardConstants.PROPERTY_SORT_STRING); - if (shouldAppendCharsetParam(sortString)) { + if (VCardConfig.isVersion30(mVCardType) && shouldAppendCharsetParam(sortString)) { + // vCard 3.0 does not force us to use UTF-8 and actually we see some + // programs which emit this value. It is incorrect from the view of + // specification, but actually necessary for parsing vCard with non-UTF-8 + // charsets, expecting other parsers not get confused with this value. mBuilder.append(VCARD_PARAM_SEPARATOR); mBuilder.append(mVCardCharsetParameter); } @@ -454,18 +641,18 @@ public class VCardBuilder { mBuilder.append(VCARD_END_OF_LINE); } else if (mIsJapaneseMobilePhone) { // Note: There is no appropriate property for expressing - // phonetic name in vCard 2.1, while there is in + // phonetic name (Yomigana in Japanese) in vCard 2.1, while there is in // vCard 3.0 (SORT-STRING). - // We chose to use DoCoMo's way when the device is Japanese one - // since it is supported by - // a lot of Japanese mobile phones. This is "X-" property, so - // any parser hopefully would not get confused with this. + // We use DoCoMo's way when the device is Japanese one since it is already + // supported by a lot of Japanese mobile phones. + // This is "X-" property, so any parser hopefully would not get + // confused with this. // // Also, DoCoMo's specification requires vCard composer to use just the first // column. // i.e. - // o SOUND;X-IRMC-N:Miyakawa Daisuke;;;; - // x SOUND;X-IRMC-N:Miyakawa;Daisuke;;; + // good: SOUND;X-IRMC-N:Miyakawa Daisuke;;;; + // bad : SOUND;X-IRMC-N:Miyakawa;Daisuke;;; mBuilder.append(VCardConstants.PROPERTY_SOUND); mBuilder.append(VCARD_PARAM_SEPARATOR); mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); @@ -519,13 +706,14 @@ public class VCardBuilder { mBuilder.append(encodedPhoneticGivenName); } } - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); // family;given + mBuilder.append(VCARD_ITEM_SEPARATOR); // given;middle + mBuilder.append(VCARD_ITEM_SEPARATOR); // middle;prefix + mBuilder.append(VCARD_ITEM_SEPARATOR); // prefix;suffix mBuilder.append(VCARD_END_OF_LINE); } + Log.d("@@@", "hoge"); if (mUsesDefactProperty) { if (!TextUtils.isEmpty(phoneticGivenName)) { final boolean reallyUseQuotedPrintable = @@ -549,7 +737,7 @@ public class VCardBuilder { mBuilder.append(VCARD_DATA_SEPARATOR); mBuilder.append(encodedPhoneticGivenName); mBuilder.append(VCARD_END_OF_LINE); - } + } // if (!TextUtils.isEmpty(phoneticGivenName)) if (!TextUtils.isEmpty(phoneticMiddleName)) { final boolean reallyUseQuotedPrintable = (mShouldUseQuotedPrintable && @@ -572,7 +760,7 @@ public class VCardBuilder { mBuilder.append(VCARD_DATA_SEPARATOR); mBuilder.append(encodedPhoneticMiddleName); mBuilder.append(VCARD_END_OF_LINE); - } + } // if (!TextUtils.isEmpty(phoneticGivenName)) if (!TextUtils.isEmpty(phoneticFamilyName)) { final boolean reallyUseQuotedPrintable = (mShouldUseQuotedPrintable && @@ -595,13 +783,13 @@ public class VCardBuilder { mBuilder.append(VCARD_DATA_SEPARATOR); mBuilder.append(encodedPhoneticFamilyName); mBuilder.append(VCARD_END_OF_LINE); - } + } // if (!TextUtils.isEmpty(phoneticFamilyName)) } } public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) { final boolean useAndroidProperty; - if (mIsV30) { + if (mIsV30OrV40) { // These specifications have NICKNAME property. useAndroidProperty = false; } else if (mUsesAndroidProperty) { useAndroidProperty = true; @@ -922,21 +1110,21 @@ public class VCardBuilder { encodedCountry = escapeCharacters(rawCountry); encodedNeighborhood = escapeCharacters(rawNeighborhood); } - final StringBuffer addressBuffer = new StringBuffer(); - addressBuffer.append(encodedPoBox); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedStreet); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedLocality); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedRegion); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedPostalCode); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedCountry); + final StringBuilder addressBuilder = new StringBuilder(); + addressBuilder.append(encodedPoBox); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street + addressBuilder.append(encodedStreet); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality + addressBuilder.append(encodedLocality); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region + addressBuilder.append(encodedRegion); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code + addressBuilder.append(encodedPostalCode); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country + addressBuilder.append(encodedCountry); return new PostalStruct( - reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); + reallyUseQuotedPrintable, appendCharset, addressBuilder.toString()); } else { // VCardUtils.areAllEmpty(rawAddressArray) == true // Try to use FORMATTED_ADDRESS instead. final String rawFormattedAddress = @@ -959,16 +1147,16 @@ public class VCardBuilder { // We use the second value ("Extended Address") just because Japanese mobile phones // do so. If the other importer expects the value be in the other field, some flag may // be needed. - final StringBuffer addressBuffer = new StringBuffer(); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedFormattedAddress); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); + final StringBuilder addressBuilder = new StringBuilder(); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address + addressBuilder.append(encodedFormattedAddress); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country return new PostalStruct( - reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); + reallyUseQuotedPrintable, appendCharset, addressBuilder.toString()); } } @@ -1108,7 +1296,8 @@ public class VCardBuilder { Log.d(LOG_TAG, "Unknown photo type. Ignored."); continue; } - final String photoString = new String(Base64.encodeBase64(data)); + // TODO: check this works fine. + final String photoString = new String(Base64.encode(data, Base64.NO_WRAP)); if (!TextUtils.isEmpty(photoString)) { appendPhotoLine(photoString, photoType); } @@ -1165,6 +1354,8 @@ public class VCardBuilder { } public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) { + // There's possibility where a given object may have more than one birthday, which + // is inappropriate. We just build one birthday. if (contentValuesList != null) { String primaryBirthday = null; String secondaryBirthday = null; @@ -1232,16 +1423,19 @@ public class VCardBuilder { return this; } + /** + * @param emitEveryTime If true, builder builds the line even when there's no entry. + */ public void appendPostalLine(final int type, final String label, final ContentValues contentValues, - final boolean isPrimary, final boolean emitLineEveryTime) { + final boolean isPrimary, final boolean emitEveryTime) { final boolean reallyUseQuotedPrintable; final boolean appendCharset; final String addressValue; { PostalStruct postalStruct = tryConstructPostalStruct(contentValues); if (postalStruct == null) { - if (emitLineEveryTime) { + if (emitEveryTime) { reallyUseQuotedPrintable = false; appendCharset = false; addressValue = ""; @@ -1463,7 +1657,7 @@ public class VCardBuilder { parameterList.add(VCardConstants.PARAM_TYPE_VOICE); } else if (VCardUtils.isMobilePhoneLabel(label)) { parameterList.add(VCardConstants.PARAM_TYPE_CELL); - } else if (mIsV30) { + } else if (mIsV30OrV40) { // This label is appropriately encoded in appendTypeParameters. parameterList.add(label); } else { @@ -1526,8 +1720,8 @@ public class VCardBuilder { StringBuilder tmpBuilder = new StringBuilder(); tmpBuilder.append(VCardConstants.PROPERTY_PHOTO); tmpBuilder.append(VCARD_PARAM_SEPARATOR); - if (mIsV30) { - tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30); + if (mIsV30OrV40) { + tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_AS_B); } else { tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21); } @@ -1559,7 +1753,8 @@ public class VCardBuilder { mBuilder.append(VCARD_END_OF_LINE); } - public void appendAndroidSpecificProperty(final String mimeType, ContentValues contentValues) { + public void appendAndroidSpecificProperty( + final String mimeType, ContentValues contentValues) { if (!sAllowedAndroidPropertySet.contains(mimeType)) { return; } @@ -1681,7 +1876,7 @@ public class VCardBuilder { encodedValue = encodeQuotedPrintable(rawValue); } else { // TODO: one line may be too huge, which may be invalid in vCard spec, though - // several (even well-known) applications do not care this. + // several (even well-known) applications do not care that violation. encodedValue = escapeCharacters(rawValue); } @@ -1744,7 +1939,12 @@ public class VCardBuilder { // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. boolean first = true; for (final String typeValue : types) { - if (VCardConfig.isV30(mVCardType)) { + if (VCardConfig.isVersion30(mVCardType)) { + final String encoded = VCardUtils.toStringAsV30ParamValue(typeValue); + if (TextUtils.isEmpty(encoded)) { + continue; + } + // Note: vCard 3.0 specifies the different type of acceptable type Strings, but // we don't emit that kind of vCard 3.0 specific type since there should be // high probabilyty in which external importers cannot understand them. @@ -1756,7 +1956,7 @@ public class VCardBuilder { } else { mBuilder.append(VCARD_PARAM_SEPARATOR); } - appendTypeParameter(VCardUtils.toStringAvailableAsV30ParameValue(typeValue)); + appendTypeParameter(encoded); } else { // vCard 2.1 if (!VCardUtils.isV21Word(typeValue)) { continue; @@ -1783,7 +1983,8 @@ public class VCardBuilder { // device is DoCoMo's (just for safety). // // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" - if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) { + if (VCardConfig.isVersion40(mVCardType) || + ((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) { builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL); } builder.append(type); @@ -1825,9 +2026,9 @@ public class VCardBuilder { byte[] strArray = null; try { - strArray = str.getBytes(mCharsetString); + strArray = str.getBytes(mCharset); } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. " + Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. " + "Try default charset"); strArray = str.getBytes(); } @@ -1893,7 +2094,7 @@ public class VCardBuilder { break; } case '\\': { - if (mIsV30) { + if (mIsV30OrV40) { tmpBuilder.append("\\\\"); break; } else { @@ -1911,7 +2112,7 @@ public class VCardBuilder { break; } case ',': { - if (mIsV30) { + if (mIsV30OrV40) { tmpBuilder.append("\\,"); } else { tmpBuilder.append(ch); diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java index 0e8b665..e409210 100644 --- a/core/java/android/pim/vcard/VCardComposer.java +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -19,16 +19,12 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Entity; -import android.content.EntityIterator; import android.content.Entity.NamedContentValues; +import android.content.EntityIterator; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.pim.vcard.exception.VCardException; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.RawContacts; -import android.provider.ContactsContract.RawContactsEntity; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Event; import android.provider.ContactsContract.CommonDataKinds.Im; @@ -41,6 +37,11 @@ import android.provider.ContactsContract.CommonDataKinds.Relation; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import android.provider.ContactsContract.RawContactsEntity; +import android.text.TextUtils; import android.util.CharsetUtils; import android.util.Log; @@ -61,15 +62,11 @@ import java.util.Map; /** * <p> - * The class for composing VCard from Contacts information. Note that this is - * completely differnt implementation from - * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore. + * The class for composing vCard from Contacts information. * </p> - * * <p> * Usually, this class should be used like this. * </p> - * * <pre class="prettyprint">VCardComposer composer = null; * try { * composer = new VCardComposer(context); @@ -93,15 +90,18 @@ import java.util.Map; * if (composer != null) { * composer.terminate(); * } - * } </pre> + * }</pre> + * <p> + * Users have to manually take care of memory efficiency. Even one vCard may contain + * image of non-trivial size for mobile devices. + * </p> + * <p> + * {@link VCardBuilder} is used to build each vCard. + * </p> */ public class VCardComposer { private static final String LOG_TAG = "VCardComposer"; - public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; - public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; - public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; - public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = "Failed to get database information"; @@ -119,6 +119,8 @@ public class VCardComposer { public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; + // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here, + // since usual vCard devices for Japanese devices already use it. private static final String SHIFT_JIS = "SHIFT_JIS"; private static final String UTF_8 = "UTF-8"; @@ -141,7 +143,7 @@ public class VCardComposer { sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); - // Google talk is a special case. + // We don't add Google talk here since it has to be handled separately. } public static interface OneEntryHandler { @@ -152,37 +154,37 @@ public class VCardComposer { /** * <p> - * An useful example handler, which emits VCard String to outputstream one by one. + * An useful handler for emitting vCard String to an OutputStream object one by one. * </p> * <p> * The input OutputStream object is closed() on {@link #onTerminate()}. - * Must not close the stream outside. + * Must not close the stream outside this class. * </p> */ - public class HandlerForOutputStream implements OneEntryHandler { + public final class HandlerForOutputStream implements OneEntryHandler { @SuppressWarnings("hiding") - private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream"; - - final private OutputStream mOutputStream; // mWriter will close this. - private Writer mWriter; + private static final String LOG_TAG = "VCardComposer.HandlerForOutputStream"; private boolean mOnTerminateIsCalled = false; + private final OutputStream mOutputStream; // mWriter will close this. + private Writer mWriter; + /** * Input stream will be closed on the detruction of this object. */ - public HandlerForOutputStream(OutputStream outputStream) { + public HandlerForOutputStream(final OutputStream outputStream) { mOutputStream = outputStream; } - public boolean onInit(Context context) { + public boolean onInit(final Context context) { try { mWriter = new BufferedWriter(new OutputStreamWriter( - mOutputStream, mCharsetString)); + mOutputStream, mCharset)); } catch (UnsupportedEncodingException e1) { - Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString); + Log.e(LOG_TAG, "Unsupported charset: " + mCharset); mErrorReason = "Encoding is not supported (usually this does not happen!): " - + mCharsetString; + + mCharset; return false; } @@ -235,14 +237,19 @@ public class VCardComposer { "IOException during closing the output stream: " + e.getMessage()); } finally { - try { - mWriter.close(); - } catch (IOException e) { - } + closeOutputStream(); } } } + public void closeOutputStream() { + try { + mWriter.close(); + } catch (IOException e) { + Log.w(LOG_TAG, "IOException is thrown during close(). Ignoring."); + } + } + @Override public void finalize() { if (!mOnTerminateIsCalled) { @@ -257,11 +264,10 @@ public class VCardComposer { private final ContentResolver mContentResolver; private final boolean mIsDoCoMo; - private final boolean mUsesShiftJis; private Cursor mCursor; private int mIdColumn; - private final String mCharsetString; + private final String mCharset; private boolean mTerminateIsCalled; private final List<OneEntryHandler> mHandlerList; @@ -272,52 +278,107 @@ public class VCardComposer { }; public VCardComposer(Context context) { - this(context, VCardConfig.VCARD_TYPE_DEFAULT, true); + this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true); } + /** + * The variant which sets charset to null and sets careHandlerErrors to true. + */ public VCardComposer(Context context, int vcardType) { - this(context, vcardType, true); + this(context, vcardType, null, true); } - public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { - this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors); + public VCardComposer(Context context, int vcardType, String charset) { + this(context, vcardType, charset, true); } /** - * Construct for supporting call log entry vCard composing. + * The variant which sets charset to null. */ public VCardComposer(final Context context, final int vcardType, final boolean careHandlerErrors) { + this(context, vcardType, null, careHandlerErrors); + } + + /** + * Construct for supporting call log entry vCard composing. + * + * @param context Context to be used during the composition. + * @param vcardType The type of vCard, typically available via {@link VCardConfig}. + * @param charset The charset to be used. Use null when you don't need the charset. + * @param careHandlerErrors If true, This object returns false everytime + * a Handler object given via {{@link #addHandler(OneEntryHandler)} returns false. + * If false, this ignores those errors. + */ + public VCardComposer(final Context context, final int vcardType, String charset, + final boolean careHandlerErrors) { mContext = context; mVCardType = vcardType; mCareHandlerErrors = careHandlerErrors; mContentResolver = context.getContentResolver(); mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); - mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); mHandlerList = new ArrayList<OneEntryHandler>(); - if (mIsDoCoMo) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - mCharsetString = charset; - } else if (mUsesShiftJis) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; + charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset); + final boolean shouldAppendCharsetParam = !( + VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset)); + + if (mIsDoCoMo || shouldAppendCharsetParam) { + if (SHIFT_JIS.equalsIgnoreCase(charset)) { + if (mIsDoCoMo) { + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, + "DoCoMo-specific SHIFT_JIS was not found. " + + "Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + } else { + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, + "Career-specific SHIFT_JIS was not found. " + + "Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + } + mCharset = charset; + } else { + Log.w(LOG_TAG, + "The charset \"" + charset + "\" is used while " + + SHIFT_JIS + " is needed to be used."); + if (TextUtils.isEmpty(charset)) { + mCharset = SHIFT_JIS; + } else { + try { + charset = CharsetUtils.charsetForVendor(charset).name(); + } catch (UnsupportedCharsetException e) { + Log.i(LOG_TAG, + "Career-specific \"" + charset + "\" was not found (as usual). " + + "Use it as is."); + } + mCharset = charset; + } } - mCharsetString = charset; } else { - mCharsetString = UTF_8; + if (TextUtils.isEmpty(charset)) { + mCharset = UTF_8; + } else { + try { + charset = CharsetUtils.charsetForVendor(charset).name(); + } catch (UnsupportedCharsetException e) { + Log.i(LOG_TAG, + "Career-specific \"" + charset + "\" was not found (as usual). " + + "Use it as is."); + } + mCharset = charset; + } } + + Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\""); } /** @@ -351,7 +412,7 @@ public class VCardComposer { } if (mCareHandlerErrors) { - List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( + final List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( mHandlerList.size()); for (OneEntryHandler handler : mHandlerList) { if (!handler.onInit(mContext)) { @@ -414,7 +475,7 @@ public class VCardComposer { mErrorReason = FAILURE_REASON_NOT_INITIALIZED; return false; } - String vcard; + final String vcard; try { if (mIdColumn >= 0) { vcard = createOneEntryInternal(mCursor.getString(mIdColumn), @@ -437,8 +498,7 @@ public class VCardComposer { mCursor.moveToNext(); } - // This function does not care the OutOfMemoryError on the handler side - // :-P + // This function does not care the OutOfMemoryError on the handler side :-P if (mCareHandlerErrors) { List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( mHandlerList.size()); @@ -457,7 +517,7 @@ public class VCardComposer { } private String createOneEntryInternal(final String contactId, - Method getEntityIteratorMethod) throws VCardException { + final Method getEntityIteratorMethod) throws VCardException { final Map<String, List<ContentValues>> contentValuesListMap = new HashMap<String, List<ContentValues>>(); // The resolver may return the entity iterator with no data. It is possible. @@ -466,12 +526,13 @@ public class VCardComposer { EntityIterator entityIterator = null; try { final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon() + // .appendQueryParameter("for_export_only", "1") .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") .build(); final String selection = Data.CONTACT_ID + "=?"; final String[] selectionArgs = new String[] {contactId}; if (getEntityIteratorMethod != null) { - // Please note that this branch is executed by some tests only + // Please note that this branch is executed by unit tests only try { entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, mContentResolver, uri, selection, selectionArgs, null); @@ -527,22 +588,33 @@ public class VCardComposer { } } - final VCardBuilder builder = new VCardBuilder(mVCardType); - builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) - .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) - .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) - .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) - .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) - .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) - .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)); - if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) { - builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)); + return buildVCard(contentValuesListMap); + } + + /** + * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in + * {ContactsContract}. Developers can override this method to customize the output. + */ + public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) { + if (contentValuesListMap == null) { + Log.e(LOG_TAG, "The given map is null. Ignore and return empty String"); + return ""; + } else { + final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset); + builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) + .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) + .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) + .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) + .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) + .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) + .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)) + .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)) + .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) + .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) + .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) + .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); + return builder.toString(); } - builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) - .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) - .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) - .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); - return builder.toString(); } public void terminate() { @@ -565,26 +637,38 @@ public class VCardComposer { @Override public void finalize() { if (!mTerminateIsCalled) { + Log.w(LOG_TAG, "terminate() is not called yet. We call it in finalize() step."); terminate(); } } + /** + * @return returns the number of available entities. The return value is undefined + * when this object is not ready yet (typically when {{@link #init()} is not called + * or when {@link #terminate()} is already called). + */ public int getCount() { if (mCursor == null) { + Log.w(LOG_TAG, "This object is not ready yet."); return 0; } return mCursor.getCount(); } + /** + * @return true when there's no entity to be built. The return value is undefined + * when this object is not ready yet. + */ public boolean isAfterLast() { if (mCursor == null) { + Log.w(LOG_TAG, "This object is not ready yet."); return false; } return mCursor.isAfterLast(); } /** - * @return Return the error reason if possible. + * @return Returns the error reason. */ public String getErrorReason() { return mErrorReason; diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java index 8219840..8b7ed6e 100644 --- a/core/java/android/pim/vcard/VCardConfig.java +++ b/core/java/android/pim/vcard/VCardConfig.java @@ -38,20 +38,43 @@ public class VCardConfig { /* package */ static final int LOG_LEVEL = LOG_LEVEL_NONE; - /* package */ static final int PARSE_TYPE_UNKNOWN = 0; - /* package */ static final int PARSE_TYPE_APPLE = 1; - /* package */ static final int PARSE_TYPE_MOBILE_PHONE_JP = 2; // For Japanese mobile phones. - /* package */ static final int PARSE_TYPE_FOMA = 3; // For Japanese FOMA mobile phones. - /* package */ static final int PARSE_TYPE_WINDOWS_MOBILE_JP = 4; - - // Assumes that "iso-8859-1" is able to map "all" 8bit characters to some unicode and - // decode the unicode to the original charset. If not, this setting will cause some bug. - public static final String DEFAULT_CHARSET = "iso-8859-1"; - - public static final int FLAG_V21 = 0; - public static final int FLAG_V30 = 1; + /** + * <p> + * The charset used during import. + * </p> + * <p> + * We cannot determine which charset should be used to interpret lines in vCard, + * while Java requires us to specify it when InputStream is used. + * We need to rely on the mechanism due to some performance reason. + * </p> + * <p> + * In order to avoid "misinterpretation" of charset and lose any data in vCard, + * "ISO-8859-1" is first used for reading the stream. + * When a charset is specified in a property (with "CHARSET=..." parameter), + * the string is decoded to raw bytes and encoded into the specific charset, + * </p> + * <p> + * Unicode specification there's a one to one mapping between each byte in ISO-8859-1 + * and a codepoint, and Java specification requires runtime must have the charset. + * Thus, ISO-8859-1 is one effective mapping for intermediate mapping. + * </p> + */ + public static final String DEFAULT_INTERMEDIATE_CHARSET = "ISO-8859-1"; - // 0x2 is reserved for the future use ... + /** + * The charset used when there's no information affbout what charset should be used to + * encode the binary given from vCard. + */ + public static final String DEFAULT_IMPORT_CHARSET = "UTF-8"; + public static final String DEFAULT_EXPORT_CHARSET = "UTF-8"; + + /** + * Do not use statically like "version == VERSION_V21" + */ + public static final int VERSION_21 = 0; + public static final int VERSION_30 = 1; + public static final int VERSION_40 = 2; + public static final int VERSION_MASK = 3; public static final int NAME_ORDER_DEFAULT = 0; public static final int NAME_ORDER_EUROPE = 0x4; @@ -59,144 +82,140 @@ public class VCardConfig { private static final int NAME_ORDER_MASK = 0xC; // 0x10 is reserved for safety - - private static final int FLAG_CHARSET_UTF8 = 0; - private static final int FLAG_CHARSET_SHIFT_JIS = 0x100; - private static final int FLAG_CHARSET_MASK = 0xF00; /** + * <p> * The flag indicating the vCard composer will add some "X-" properties used only in Android * when the formal vCard specification does not have appropriate fields for that data. - * + * </p> + * <p> * For example, Android accepts nickname information while vCard 2.1 does not. * When this flag is on, vCard composer emits alternative "X-" property (like "X-NICKNAME") * instead of just dropping it. - * + * </p> + * <p> * vCard parser code automatically parses the field emitted even when this flag is off. - * - * Note that this flag does not assure all the information must be hold in the emitted vCard. + * </p> */ private static final int FLAG_USE_ANDROID_PROPERTY = 0x80000000; /** + * <p> * The flag indicating the vCard composer will add some "X-" properties seen in the * vCard data emitted by the other softwares/devices when the formal vCard specification - * does not have appropriate field(s) for that data. - * + * does not have appropriate field(s) for that data. + * </p> + * <p> * One example is X-PHONETIC-FIRST-NAME/X-PHONETIC-MIDDLE-NAME/X-PHONETIC-LAST-NAME, which are * for phonetic name (how the name is pronounced), seen in the vCard emitted by some other * non-Android devices/softwares. We chose to enable the vCard composer to use those * defact properties since they are also useful for Android devices. - * + * </p> + * <p> * Note for developers: only "X-" properties should be added with this flag. vCard 2.1/3.0 * allows any kind of "X-" properties but does not allow non-"X-" properties (except IANA tokens * in vCard 3.0). Some external parsers may get confused with non-valid, non-"X-" properties. + * </p> */ private static final int FLAG_USE_DEFACT_PROPERTY = 0x40000000; /** - * The flag indicating some specific dialect seen in vcard of DoCoMo (one of Japanese + * <p> + * The flag indicating some specific dialect seen in vCard of DoCoMo (one of Japanese * mobile careers) should be used. This flag does not include any other information like * that "the vCard is for Japanese". So it is "possible" that "the vCard should have DoCoMo's * dialect but the name order should be European", but it is not recommended. + * </p> */ private static final int FLAG_DOCOMO = 0x20000000; /** - * <P> + * <p> * The flag indicating the vCard composer does "NOT" use Quoted-Printable toward "primary" * properties even though it is required by vCard 2.1 (QP is prohibited in vCard 3.0). - * </P> - * <P> + * </p> + * <p> * We actually cannot define what is the "primary" property. Note that this is NOT defined * in vCard specification either. Also be aware that it is NOT related to "primary" notion * used in {@link android.provider.ContactsContract}. * This notion is just for vCard composition in Android. - * </P> - * <P> + * </p> + * <p> * We added this Android-specific notion since some (incomplete) vCard exporters for vCard 2.1 * do NOT use Quoted-Printable encoding toward some properties related names like "N", "FN", etc. * even when their values contain non-ascii or/and CR/LF, while they use the encoding in the * other properties like "ADR", "ORG", etc. - * <P> + * <p> * We are afraid of the case where some vCard importer also forget handling QP presuming QP is * not used in such fields. - * </P> - * <P> + * </p> + * <p> * This flag is useful when some target importer you are going to focus on does not accept * such properties with Quoted-Printable encoding. - * </P> - * <P> + * </p> + * <p> * Again, we should not use this flag at all for complying vCard 2.1 spec. - * </P> - * <P> + * </p> + * <p> * In vCard 3.0, Quoted-Printable is explicitly "prohibitted", so we don't need to care this * kind of problem (hopefully). - * </P> + * </p> + * @hide */ public static final int FLAG_REFRAIN_QP_TO_NAME_PROPERTIES = 0x10000000; /** - * <P> + * <p> * The flag indicating that phonetic name related fields must be converted to * appropriate form. Note that "appropriate" is not defined in any vCard specification. * This is Android-specific. - * </P> - * <P> + * </p> + * <p> * One typical (and currently sole) example where we need this flag is the time when * we need to emit Japanese phonetic names into vCard entries. The property values * should be encoded into half-width katakana when the target importer is Japanese mobile * phones', which are probably not able to parse full-width hiragana/katakana for * historical reasons, while the vCard importers embedded to softwares for PC should be * able to parse them as we expect. - * </P> + * </p> */ - public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x0800000; + public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x08000000; /** - * <P> + * <p> * The flag indicating the vCard composer "for 2.1" emits "TYPE=" string toward TYPE params * every time possible. The default behavior does not emit it and is valid in the spec. * In vCrad 3.0, this flag is unnecessary, since "TYPE=" is MUST in vCard 3.0 specification. - * </P> - * <P> + * </p> + * <p> * Detail: * How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0. * </p> - * <P> - * e.g.<BR /> - * 1) Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..."<BR /> - * 2) Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..."<BR /> - * 3) Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..."<BR /> - * </P> - * <P> - * 2) had been the default of VCard exporter/importer in Android, but it is found that - * some external exporter is not able to parse the type format like 2) but only 3). - * </P> - * <P> + * <p> + * e.g. + * </p> + * <ol> + * <li>Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..."</li> + * <li>Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..."</li> + * <li>Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..."</li> + * </ol> + * <p> * If you are targeting to the importer which cannot accept TYPE params without "TYPE=" * strings (which should be rare though), please use this flag. - * </P> - * <P> - * Example usage: int vcardType = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM); - * </P> + * </p> + * <p> + * Example usage: + * <pre class="prettyprint">int type = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM);</pre> + * </p> */ public static final int FLAG_APPEND_TYPE_PARAM = 0x04000000; /** - * <P> - * The flag asking exporter to refrain image export. - * </P> - * @hide will be deleted in the near future. - */ - public static final int FLAG_REFRAIN_IMAGE_EXPORT = 0x02000000; - - /** - * <P> + * <p> * The flag indicating the vCard composer does touch nothing toward phone number Strings * but leave it as is. - * </P> - * <P> + * </p> + * <p> * The vCard specifications mention nothing toward phone numbers, while some devices * do (wrongly, but with innevitable reasons). * For example, there's a possibility Japanese mobile phones are expected to have @@ -207,187 +226,194 @@ public class VCardConfig { * becomes "111-222-3333"). * Unfortunate side effect of that use was some control characters used in the other * areas may be badly affected by the formatting. - * </P> - * <P> + * </p> + * <p> * This flag disables that formatting, affecting both importer and exporter. * If the user is aware of some side effects due to the implicit formatting, use this flag. - * </P> + * </p> */ public static final int FLAG_REFRAIN_PHONE_NUMBER_FORMATTING = 0x02000000; + /** + * <p> + * For importer only. Ignored in exporter. + * </p> + * <p> + * The flag indicating the parser should handle a nested vCard, in which vCard clause starts + * in another vCard clause. Here's a typical example. + * </p> + * <pre class="prettyprint">BEGIN:VCARD + * BEGIN:VCARD + * VERSION:2.1 + * ... + * END:VCARD + * END:VCARD</pre> + * <p> + * The vCard 2.1 specification allows the nest, but also let parsers ignore nested entries, + * while some mobile devices emit nested ones as primary data to be imported. + * </p> + * <p> + * This flag forces a vCard parser to torelate such a nest and understand its content. + * </p> + */ + public static final int FLAG_TORELATE_NEST = 0x01000000; + //// The followings are VCard types available from importer/exporter. //// /** - * <P> - * Generic vCard format with the vCard 2.1. Uses UTF-8 for the charset. - * When composing a vCard entry, the US convension will be used toward formatting - * some values. - * </P> - * <P> + * <p> + * The type indicating nothing. Used by {@link VCardSourceDetector} when it + * was not able to guess the exact vCard type. + * </p> + */ + public static final int VCARD_TYPE_UNKNOWN = 0; + + /** + * <p> + * Generic vCard format with the vCard 2.1. When composing a vCard entry, + * the US convension will be used toward formatting some values. + * </p> + * <p> * e.g. The order of the display name would be "Prefix Given Middle Family Suffix", * while it should be "Prefix Family Middle Given Suffix" in Japan for example. - * </P> + * </p> + * <p> + * Uses UTF-8 for the charset as a charset for exporting. Note that old vCard importer + * outside Android cannot accept it since vCard 2.1 specifically does not allow + * that charset, while we need to use it to support various languages around the world. + * </p> + * <p> + * If you want to use alternative charset, you should notify the charset to the other + * compontent to be used. + * </p> */ - public static final int VCARD_TYPE_V21_GENERIC_UTF8 = - (FLAG_V21 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + public static final int VCARD_TYPE_V21_GENERIC = + (VERSION_21 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static String VCARD_TYPE_V21_GENERIC_UTF8_STR = "v21_generic"; + /* package */ static String VCARD_TYPE_V21_GENERIC_STR = "v21_generic"; /** - * <P> + * <p> * General vCard format with the version 3.0. Uses UTF-8 for the charset. - * </P> - * <P> + * </p> + * <p> * Not fully ready yet. Use with caution when you use this. - * </P> + * </p> */ - public static final int VCARD_TYPE_V30_GENERIC_UTF8 = - (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + public static final int VCARD_TYPE_V30_GENERIC = + (VERSION_30 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V30_GENERIC_STR = "v30_generic"; + + /** + * General vCard format with the version 4.0. + * @hide vCard 4.0 is not published yet. + */ + public static final int VCARD_TYPE_V40_GENERIC = + (VERSION_40 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V40_GENERIC_STR = "v40_generic"; - /* package */ static final String VCARD_TYPE_V30_GENERIC_UTF8_STR = "v30_generic"; - /** - * <P> + * <p> * General vCard format for the vCard 2.1 with some Europe convension. Uses Utf-8. * Currently, only name order is considered ("Prefix Middle Given Family Suffix") - * </P> + * </p> */ - public static final int VCARD_TYPE_V21_EUROPE_UTF8 = - (FLAG_V21 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V21_EUROPE_UTF8_STR = "v21_europe"; + public static final int VCARD_TYPE_V21_EUROPE = + (VERSION_21 | NAME_ORDER_EUROPE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_EUROPE_STR = "v21_europe"; /** - * <P> + * <p> * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8. - * </P> - * <P> + * </p> + * <p> * Not ready yet. Use with caution when you use this. - * </P> + * </p> */ - public static final int VCARD_TYPE_V30_EUROPE_UTF8 = - (FLAG_V30 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + public static final int VCARD_TYPE_V30_EUROPE = + (VERSION_30 | NAME_ORDER_EUROPE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); /* package */ static final String VCARD_TYPE_V30_EUROPE_STR = "v30_europe"; /** - * <P> + * <p> * The vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. - * </P> - * <P> + * </p> + * <p> * Not ready yet. Use with caution when you use this. - * </P> + * </p> */ - public static final int VCARD_TYPE_V21_JAPANESE_UTF8 = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V21_JAPANESE_UTF8_STR = "v21_japanese_utf8"; + public static final int VCARD_TYPE_V21_JAPANESE = + (VERSION_21 | NAME_ORDER_JAPANESE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /** - * <P> - * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for - * parsing/composing the vCard data. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V21_JAPANESE_SJIS = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + /* package */ static final String VCARD_TYPE_V21_JAPANESE_STR = "v21_japanese_utf8"; - /* package */ static final String VCARD_TYPE_V21_JAPANESE_SJIS_STR = "v21_japanese_sjis"; - - /** - * <P> - * vCard format for miscellaneous Japanese devices, using Shift_Jis for - * parsing/composing the vCard data. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V30_JAPANESE_SJIS = - (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V30_JAPANESE_SJIS_STR = "v30_japanese_sjis"; - /** - * <P> + * <p> * The vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. - * </P> - * <P> + * </p> + * <p> * Not ready yet. Use with caution when you use this. - * </P> + * </p> */ - public static final int VCARD_TYPE_V30_JAPANESE_UTF8 = - (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + public static final int VCARD_TYPE_V30_JAPANESE = + (VERSION_30 | NAME_ORDER_JAPANESE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V30_JAPANESE_UTF8_STR = "v30_japanese_utf8"; + /* package */ static final String VCARD_TYPE_V30_JAPANESE_STR = "v30_japanese_utf8"; /** - * <P> + * <p> * The vCard 2.1 based format which (partially) considers the convention in Japanese * mobile phones, where phonetic names are translated to half-width katakana if - * possible, etc. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> + * possible, etc. It would be better to use Shift_JIS as a charset for maximum + * compatibility. + * </p> + * @hide Should not be available world wide. */ public static final int VCARD_TYPE_V21_JAPANESE_MOBILE = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_CONVERT_PHONETIC_NAME_STRINGS | - FLAG_REFRAIN_QP_TO_NAME_PROPERTIES); + (VERSION_21 | NAME_ORDER_JAPANESE | + FLAG_CONVERT_PHONETIC_NAME_STRINGS | FLAG_REFRAIN_QP_TO_NAME_PROPERTIES); /* package */ static final String VCARD_TYPE_V21_JAPANESE_MOBILE_STR = "v21_japanese_mobile"; /** - * <P> - * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. + * <p> + * The vCard format used in DoCoMo, which is one of Japanese mobile phone careers. * </p> - * <P> + * <p> * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. * No Android-specific property nor defact property is included. The "Primary" properties * are NOT encoded to Quoted-Printable. - * </P> + * </p> + * @hide Should not be available world wide. */ public static final int VCARD_TYPE_DOCOMO = (VCARD_TYPE_V21_JAPANESE_MOBILE | FLAG_DOCOMO); /* package */ static final String VCARD_TYPE_DOCOMO_STR = "docomo"; - public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC_UTF8; + public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC; private static final Map<String, Integer> sVCardTypeMap; private static final Set<Integer> sJapaneseMobileTypeSet; static { sVCardTypeMap = new HashMap<String, Integer>(); - sVCardTypeMap.put(VCARD_TYPE_V21_GENERIC_UTF8_STR, VCARD_TYPE_V21_GENERIC_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V30_GENERIC_UTF8_STR, VCARD_TYPE_V30_GENERIC_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V21_EUROPE_UTF8_STR, VCARD_TYPE_V21_EUROPE_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_SJIS_STR, VCARD_TYPE_V21_JAPANESE_SJIS); - sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_SJIS_STR, VCARD_TYPE_V30_JAPANESE_SJIS); - sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_GENERIC_STR, VCARD_TYPE_V21_GENERIC); + sVCardTypeMap.put(VCARD_TYPE_V30_GENERIC_STR, VCARD_TYPE_V30_GENERIC); + sVCardTypeMap.put(VCARD_TYPE_V21_EUROPE_STR, VCARD_TYPE_V21_EUROPE); + sVCardTypeMap.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_STR, VCARD_TYPE_V21_JAPANESE); + sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_STR, VCARD_TYPE_V30_JAPANESE); sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_MOBILE_STR, VCARD_TYPE_V21_JAPANESE_MOBILE); sVCardTypeMap.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); sJapaneseMobileTypeSet = new HashSet<Integer>(); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_UTF8); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_SJIS); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_UTF8); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE); sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_MOBILE); sJapaneseMobileTypeSet.add(VCARD_TYPE_DOCOMO); } @@ -404,20 +430,20 @@ public class VCardConfig { } } - public static boolean isV30(final int vcardType) { - return ((vcardType & FLAG_V30) != 0); + public static boolean isVersion21(final int vcardType) { + return (vcardType & VERSION_MASK) == VERSION_21; } - public static boolean shouldUseQuotedPrintable(final int vcardType) { - return !isV30(vcardType); + public static boolean isVersion30(final int vcardType) { + return (vcardType & VERSION_MASK) == VERSION_30; } - public static boolean usesUtf8(final int vcardType) { - return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_UTF8); + public static boolean isVersion40(final int vcardType) { + return (vcardType & VERSION_MASK) == VERSION_40; } - public static boolean usesShiftJis(final int vcardType) { - return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_SHIFT_JIS); + public static boolean shouldUseQuotedPrintable(final int vcardType) { + return !isVersion30(vcardType); } public static int getNameOrderType(final int vcardType) { @@ -442,7 +468,7 @@ public class VCardConfig { } public static boolean appendTypeParamName(final int vcardType) { - return (isV30(vcardType) || ((vcardType & FLAG_APPEND_TYPE_PARAM) != 0)); + return (isVersion30(vcardType) || ((vcardType & FLAG_APPEND_TYPE_PARAM) != 0)); } /** @@ -474,4 +500,4 @@ public class VCardConfig { private VCardConfig() { } -} +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardConstants.java b/core/java/android/pim/vcard/VCardConstants.java index 8c07126..76371ef 100644 --- a/core/java/android/pim/vcard/VCardConstants.java +++ b/core/java/android/pim/vcard/VCardConstants.java @@ -21,6 +21,7 @@ package android.pim.vcard; public class VCardConstants { public static final String VERSION_V21 = "2.1"; public static final String VERSION_V30 = "3.0"; + public static final String VERSION_V40 = "4.0"; // The property names valid both in vCard 2.1 and 3.0. public static final String PROPERTY_BEGIN = "BEGIN"; @@ -38,25 +39,30 @@ public class VCardConstants { public static final String PROPERTY_PHOTO = "PHOTO"; public static final String PROPERTY_LOGO = "LOGO"; public static final String PROPERTY_URL = "URL"; - public static final String PROPERTY_BDAY = "BDAY"; // Birthday + public static final String PROPERTY_BDAY = "BDAY"; // Birthday (3.0, 4.0) + public static final String PROPERTY_BIRTH = "BIRTH"; // Place of birth (4.0) + public static final String PROPERTY_ANNIVERSARY = "ANNIVERSARY"; // Date of marriage (4.0) + public static final String PROPERTY_NAME = "NAME"; // (3.0, 4,0) + public static final String PROPERTY_NICKNAME = "NICKNAME"; // (3.0, 4.0) + public static final String PROPERTY_SORT_STRING = "SORT-STRING"; // (3.0, 4.0) public static final String PROPERTY_END = "END"; - // Valid property names not supported (not appropriately handled) by our vCard importer now. + // Valid property names not supported (not appropriately handled) by our importer. + // TODO: Should be removed from the view of memory efficiency? public static final String PROPERTY_REV = "REV"; - public static final String PROPERTY_AGENT = "AGENT"; + public static final String PROPERTY_AGENT = "AGENT"; // (3.0) + public static final String PROPERTY_DDAY = "DDAY"; // Date of death (4.0) + public static final String PROPERTY_DEATH = "DEATH"; // Place of death (4.0) // Available in vCard 3.0. Shoud not use when composing vCard 2.1 file. - public static final String PROPERTY_NAME = "NAME"; - public static final String PROPERTY_NICKNAME = "NICKNAME"; - public static final String PROPERTY_SORT_STRING = "SORT-STRING"; // De-fact property values expressing phonetic names. public static final String PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; public static final String PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; public static final String PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; - // Properties both ContactsStruct in Eclair and de-fact vCard extensions - // shown in http://en.wikipedia.org/wiki/VCard support are defined here. + // Properties both ContactsStruct and de-fact vCard extensions + // Shown in http://en.wikipedia.org/wiki/VCard support are defined here. public static final String PROPERTY_X_AIM = "X-AIM"; public static final String PROPERTY_X_MSN = "X-MSN"; public static final String PROPERTY_X_YAHOO = "X-YAHOO"; @@ -89,6 +95,9 @@ public class VCardConstants { public static final String PARAM_TYPE_VOICE = "VOICE"; public static final String PARAM_TYPE_INTERNET = "INTERNET"; + public static final String PARAM_CHARSET = "CHARSET"; + public static final String PARAM_ENCODING = "ENCODING"; + // Abbreviation of "prefered" according to vCard 2.1 specification. // We interpret this value as "primary" property during import/export. // @@ -109,6 +118,12 @@ public class VCardConstants { public static final String PARAM_TYPE_BBS = "BBS"; public static final String PARAM_TYPE_VIDEO = "VIDEO"; + public static final String PARAM_ENCODING_7BIT = "7BIT"; + public static final String PARAM_ENCODING_8BIT = "8BIT"; + public static final String PARAM_ENCODING_QP = "QUOTED-PRINTABLE"; + public static final String PARAM_ENCODING_BASE64 = "BASE64"; // Available in vCard 2.1 + public static final String PARAM_ENCODING_B = "B"; // Available in vCard 3.0 + // TYPE parameters for Phones, which are not formally valid in vCard (at least 2.1). // These types are basically encoded to "X-" parameters when composing vCard. // Parser passes these when "X-" is added to the parameter or not. @@ -126,14 +141,15 @@ public class VCardConstants { public static final String PARAM_ADR_TYPE_DOM = "DOM"; public static final String PARAM_ADR_TYPE_INTL = "INTL"; + public static final String PARAM_LANGUAGE = "LANGUAGE"; + + // SORT-AS parameter introduced in vCard 4.0 (as of rev.13) + public static final String PARAM_SORT_AS = "SORT-AS"; + // TYPE parameters not officially valid but used in some vCard exporter. // Do not use in composer side. public static final String PARAM_EXTRA_TYPE_COMPANY = "COMPANY"; - // DoCoMo specific type parameter. Used with "SOUND" property, which is alternate of SORT-STRING in - // vCard 3.0. - public static final String PARAM_TYPE_X_IRMC_N = "X-IRMC-N"; - public interface ImportOnly { public static final String PROPERTY_X_NICKNAME = "X-NICKNAME"; // Some device emits this "X-" parameter for expressing Google Talk, @@ -142,7 +158,14 @@ public class VCardConstants { public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; } - /* package */ static final int MAX_DATA_COLUMN = 15; + //// Mainly for package constants. + + // DoCoMo specific type parameter. Used with "SOUND" property, which is alternate of + // SORT-STRING invCard 3.0. + /* package */ static final String PARAM_TYPE_X_IRMC_N = "X-IRMC-N"; + + // Used in unit test. + public static final int MAX_DATA_COLUMN = 15; /* package */ static final int MAX_CHARACTER_NUMS_QP = 76; static final int MAX_CHARACTER_NUMS_BASE64_V30 = 75; diff --git a/core/java/android/pim/vcard/VCardEntry.java b/core/java/android/pim/vcard/VCardEntry.java index 97d3fbc..94f7c5f 100644 --- a/core/java/android/pim/vcard/VCardEntry.java +++ b/core/java/android/pim/vcard/VCardEntry.java @@ -20,13 +20,11 @@ import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentResolver; import android.content.OperationApplicationException; -import android.database.Cursor; import android.net.Uri; import android.os.RemoteException; import android.provider.ContactsContract; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.Groups; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Event; @@ -61,9 +59,6 @@ public class VCardEntry { private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK; - private static final String ACCOUNT_TYPE_GOOGLE = "com.google"; - private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts"; - private static final Map<String, Integer> sImMap = new HashMap<String, Integer>(); static { @@ -78,12 +73,12 @@ public class VCardEntry { Im.PROTOCOL_GOOGLE_TALK); } - static public class PhoneData { + public static class PhoneData { public final int type; public final String data; public final String label; - // isPrimary is changable only when there's no appropriate one existing in - // the original VCard. + // isPrimary is (not final but) changable, only when there's no appropriate one existing + // in the original VCard. public boolean isPrimary; public PhoneData(int type, String data, String label, boolean isPrimary) { this.type = type; @@ -109,13 +104,11 @@ public class VCardEntry { } } - static public class EmailData { + public static class EmailData { public final int type; public final String data; // Used only when TYPE is TYPE_CUSTOM. public final String label; - // isPrimary is changable only when there's no appropriate one existing in - // the original VCard. public boolean isPrimary; public EmailData(int type, String data, String label, boolean isPrimary) { this.type = type; @@ -141,9 +134,9 @@ public class VCardEntry { } } - static public class PostalData { - // Determined by vCard spec. - // PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name + public static class PostalData { + // Determined by vCard specification. + // - PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name public static final int ADDR_MAX_DATA_SIZE = 7; private final String[] dataArray; public final String pobox; @@ -248,24 +241,28 @@ public class VCardEntry { } } - static public class OrganizationData { + public static class OrganizationData { public final int type; // non-final is Intentional: we may change the values since this info is separated into - // two parts in vCard: "ORG" + "TITLE". + // two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in + // different timing. public String companyName; public String departmentName; public String titleName; + public final String phoneticName; // We won't have this in "TITLE" property. public boolean isPrimary; public OrganizationData(int type, - String companyName, - String departmentName, - String titleName, - boolean isPrimary) { + final String companyName, + final String departmentName, + final String titleName, + final String phoneticName, + final boolean isPrimary) { this.type = type; this.companyName = companyName; this.departmentName = departmentName; this.titleName = titleName; + this.phoneticName = phoneticName; this.isPrimary = isPrimary; } @@ -313,7 +310,7 @@ public class VCardEntry { } } - static public class ImData { + public static class ImData { public final int protocol; public final String customProtocol; public final int type; @@ -434,6 +431,14 @@ public class VCardEntry { } } + // TODO(dmiyakawa): vCard 4.0 logically has multiple formatted names and we need to + // select the most preferable one using PREF parameter. + // + // e.g. (based on rev.13) + // FN;PREF=1:John M. Doe + // FN;PREF=2:John Doe + // FN;PREF=3;John + private String mFamilyName; private String mGivenName; private String mMiddleName; @@ -441,7 +446,7 @@ public class VCardEntry { private String mSuffix; // Used only when no family nor given name is found. - private String mFullName; + private String mFormattedName; private String mPhoneticFamilyName; private String mPhoneticGivenName; @@ -454,6 +459,7 @@ public class VCardEntry { private String mDisplayName; private String mBirthday; + private String mAnniversary; private List<String> mNoteList; private List<PhoneData> mPhoneList; @@ -469,7 +475,7 @@ public class VCardEntry { private final Account mAccount; public VCardEntry() { - this(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + this(VCardConfig.VCARD_TYPE_V21_GENERIC); } public VCardEntry(int vcardType) { @@ -499,7 +505,6 @@ public class VCardEntry { } } - // Use NANP in default when there's no information about locale. final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType); formattedNumber = PhoneNumberUtils.formatNumber(builder.toString(), formattingType); } @@ -529,22 +534,44 @@ public class VCardEntry { } /** - * Should be called via {@link #handleOrgValue(int, List, boolean)} or + * Should be called via {@link #handleOrgValue(int, List, Map, boolean) or * {@link #handleTitleValue(String)}. */ private void addNewOrganization(int type, final String companyName, final String departmentName, - final String titleName, boolean isPrimary) { + final String titleName, + final String phoneticName, + final boolean isPrimary) { if (mOrganizationList == null) { mOrganizationList = new ArrayList<OrganizationData>(); } mOrganizationList.add(new OrganizationData(type, companyName, - departmentName, titleName, isPrimary)); + departmentName, titleName, phoneticName, isPrimary)); } private static final List<String> sEmptyList = Collections.unmodifiableList(new ArrayList<String>(0)); + private String buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap) { + final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS); + if (sortAsCollection != null && sortAsCollection.size() != 0) { + if (sortAsCollection.size() > 1) { + Log.w(LOG_TAG, "Incorrect multiple SORT_AS parameters detected: " + + Arrays.toString(sortAsCollection.toArray())); + } + final List<String> sortNames = + VCardUtils.constructListFromValue(sortAsCollection.iterator().next(), + mVCardType); + final StringBuilder builder = new StringBuilder(); + for (final String elem : sortNames) { + builder.append(elem); + } + return builder.toString(); + } else { + return null; + } + } + /** * Set "ORG" related values to the appropriate data. If there's more than one * {@link OrganizationData} objects, this input data are attached to the last one which @@ -552,7 +579,9 @@ public class VCardEntry { * {@link OrganizationData} object, a new {@link OrganizationData} is created, * whose title is set to null. */ - private void handleOrgValue(final int type, List<String> orgList, boolean isPrimary) { + private void handleOrgValue(final int type, List<String> orgList, + Map<String, Collection<String>> paramMap, boolean isPrimary) { + final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap); if (orgList == null) { orgList = sEmptyList; } @@ -587,7 +616,7 @@ public class VCardEntry { if (mOrganizationList == null) { // Create new first organization entry, with "null" title which may be // added via handleTitleValue(). - addNewOrganization(type, companyName, departmentName, null, isPrimary); + addNewOrganization(type, companyName, departmentName, null, phoneticName, isPrimary); return; } for (OrganizationData organizationData : mOrganizationList) { @@ -605,7 +634,7 @@ public class VCardEntry { } // No OrganizatioData is available. Create another one, with "null" title, which may be // added via handleTitleValue(). - addNewOrganization(type, companyName, departmentName, null, isPrimary); + addNewOrganization(type, companyName, departmentName, null, phoneticName, isPrimary); } /** @@ -619,7 +648,7 @@ public class VCardEntry { if (mOrganizationList == null) { // Create new first organization entry, with "null" other info, which may be // added via handleOrgValue(). - addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false); + addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, null, false); return; } for (OrganizationData organizationData : mOrganizationList) { @@ -630,7 +659,7 @@ public class VCardEntry { } // No Organization is available. Create another one, with "null" other info, which may be // added via handleOrgValue(). - addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false); + addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, null, false); } private void addIm(int protocol, String customProtocol, int type, @@ -656,11 +685,54 @@ public class VCardEntry { mPhotoList.add(photoData); } + /** + * Tries to extract paramMap, constructs SORT-AS parameter values, and store them in + * appropriate phonetic name variables. + * + * This method does not care the vCard version. Even when we have SORT-AS parameters in + * invalid versions (i.e. 2.1 and 3.0), we scilently accept them so that we won't drop + * meaningful information. If we had this parameter in the N field of vCard 3.0, and + * the contact data also have SORT-STRING, we will prefer SORT-STRING, since it is + * regitimate property to be understood. + */ + private void tryHandleSortAsName(final Map<String, Collection<String>> paramMap) { + if (VCardConfig.isVersion30(mVCardType) && + !(TextUtils.isEmpty(mPhoneticFamilyName) && + TextUtils.isEmpty(mPhoneticMiddleName) && + TextUtils.isEmpty(mPhoneticGivenName))) { + return; + } + + final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS); + if (sortAsCollection != null && sortAsCollection.size() != 0) { + if (sortAsCollection.size() > 1) { + Log.w(LOG_TAG, "Incorrect multiple SORT_AS parameters detected: " + + Arrays.toString(sortAsCollection.toArray())); + } + final List<String> sortNames = + VCardUtils.constructListFromValue(sortAsCollection.iterator().next(), + mVCardType); + int size = sortNames.size(); + if (size > 3) { + size = 3; + } + switch (size) { + case 3: mPhoneticMiddleName = sortNames.get(2); //$FALL-THROUGH$ + case 2: mPhoneticGivenName = sortNames.get(1); //$FALL-THROUGH$ + default: mPhoneticFamilyName = sortNames.get(0); break; + } + } + } + @SuppressWarnings("fallthrough") - private void handleNProperty(List<String> elems) { + private void handleNProperty(final List<String> paramValues, + Map<String, Collection<String>> paramMap) { + // in vCard 4.0, SORT-AS parameter is available. + tryHandleSortAsName(paramMap); + // Family, Given, Middle, Prefix, Suffix. (1 - 5) int size; - if (elems == null || (size = elems.size()) < 1) { + if (paramValues == null || (size = paramValues.size()) < 1) { return; } if (size > 5) { @@ -668,12 +740,12 @@ public class VCardEntry { } switch (size) { - // fallthrough - case 5: mSuffix = elems.get(4); - case 4: mPrefix = elems.get(3); - case 3: mMiddleName = elems.get(2); - case 2: mGivenName = elems.get(1); - default: mFamilyName = elems.get(0); + // Fall-through. + case 5: mSuffix = paramValues.get(4); + case 4: mPrefix = paramValues.get(3); + case 3: mMiddleName = paramValues.get(2); + case 2: mGivenName = paramValues.get(1); + default: mFamilyName = paramValues.get(0); } } @@ -754,13 +826,13 @@ public class VCardEntry { if (propName.equals(VCardConstants.PROPERTY_VERSION)) { // vCard version. Ignore this. } else if (propName.equals(VCardConstants.PROPERTY_FN)) { - mFullName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFullName == null) { + mFormattedName = propValue; + } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFormattedName == null) { // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not // actually exist in the real vCard data, does not exist. - mFullName = propValue; + mFormattedName = propValue; } else if (propName.equals(VCardConstants.PROPERTY_N)) { - handleNProperty(propValueList); + handleNProperty(propValueList, paramMap); } else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) { mPhoneticFullName = propValue; } else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) || @@ -775,8 +847,7 @@ public class VCardEntry { // which is correct behavior from the view of vCard 2.1. // But we want it to be separated, so do the separation here. final List<String> phoneticNameList = - VCardUtils.constructListFromValue(propValue, - VCardConfig.isV30(mVCardType)); + VCardUtils.constructListFromValue(propValue, mVCardType); handlePhoneticNameFromSound(phoneticNameList); } else { // Ignore this field since Android cannot understand what it is. @@ -877,7 +948,7 @@ public class VCardEntry { } } } - handleOrgValue(type, propValueList, isPrimary); + handleOrgValue(type, propValueList, paramMap, isPrimary); } else if (propName.equals(VCardConstants.PROPERTY_TITLE)) { handleTitleValue(propValue); } else if (propName.equals(VCardConstants.PROPERTY_ROLE)) { @@ -966,6 +1037,8 @@ public class VCardEntry { mWebsiteList.add(propValue); } else if (propName.equals(VCardConstants.PROPERTY_BDAY)) { mBirthday = propValue; + } else if (propName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) { + mAnniversary = propValue; } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) { mPhoneticGivenName = propValue; } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { @@ -974,33 +1047,9 @@ public class VCardEntry { mPhoneticFamilyName = propValue; } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) { final List<String> customPropertyList = - VCardUtils.constructListFromValue(propValue, - VCardConfig.isV30(mVCardType)); + VCardUtils.constructListFromValue(propValue, mVCardType); handleAndroidCustomProperty(customPropertyList); - /*} else if (propName.equals("REV")) { - // Revision of this VCard entry. I think we can ignore this. - } else if (propName.equals("UID")) { - } else if (propName.equals("KEY")) { - // Type is X509 or PGP? I don't know how to handle this... - } else if (propName.equals("MAILER")) { - } else if (propName.equals("TZ")) { - } else if (propName.equals("GEO")) { - } else if (propName.equals("CLASS")) { - // vCard 3.0 only. - // e.g. CLASS:CONFIDENTIAL - } else if (propName.equals("PROFILE")) { - // VCard 3.0 only. Must be "VCARD". I think we can ignore this. - } else if (propName.equals("CATEGORIES")) { - // VCard 3.0 only. - // e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY - } else if (propName.equals("SOURCE")) { - // VCard 3.0 only. - } else if (propName.equals("PRODID")) { - // VCard 3.0 only. - // To specify the identifier for the product that created - // the vCard object.*/ } else { - // Unknown X- words and IANA token. } } @@ -1016,8 +1065,8 @@ public class VCardEntry { */ private void constructDisplayName() { // FullName (created via "FN" or "NAME" field) is prefered. - if (!TextUtils.isEmpty(mFullName)) { - mDisplayName = mFullName; + if (!TextUtils.isEmpty(mFormattedName)) { + mDisplayName = mFormattedName; } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { mDisplayName = VCardUtils.constructNameFromElements(mVCardType, mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix); @@ -1062,23 +1111,6 @@ public class VCardEntry { if (mAccount != null) { builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name); builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type); - - // Assume that caller side creates this group if it does not exist. - if (ACCOUNT_TYPE_GOOGLE.equals(mAccount.type)) { - final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] { - Groups.SOURCE_ID }, - Groups.TITLE + "=?", new String[] { - GOOGLE_MY_CONTACTS_GROUP }, null); - try { - if (cursor != null && cursor.moveToFirst()) { - myGroupsId = cursor.getString(0); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } } else { builder.withValue(RawContacts.ACCOUNT_NAME, null); builder.withValue(RawContacts.ACCOUNT_TYPE, null); @@ -1154,6 +1186,9 @@ public class VCardEntry { if (organizationData.titleName != null) { builder.withValue(Organization.TITLE, organizationData.titleName); } + if (organizationData.phoneticName != null) { + builder.withValue(Organization.PHONETIC_NAME, organizationData.phoneticName); + } if (organizationData.isPrimary) { builder.withValue(Organization.IS_PRIMARY, 1); } @@ -1251,6 +1286,15 @@ public class VCardEntry { operationList.add(builder.build()); } + if (!TextUtils.isEmpty(mAnniversary)) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Event.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE); + builder.withValue(Event.START_DATE, mAnniversary); + builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY); + operationList.add(builder.build()); + } + if (mAndroidCustomPropertyList != null) { for (List<String> customPropertyList : mAndroidCustomPropertyList) { int size = customPropertyList.size(); @@ -1322,7 +1366,7 @@ public class VCardEntry { && TextUtils.isEmpty(mGivenName) && TextUtils.isEmpty(mPrefix) && TextUtils.isEmpty(mSuffix) - && TextUtils.isEmpty(mFullName) + && TextUtils.isEmpty(mFormattedName) && TextUtils.isEmpty(mPhoneticFamilyName) && TextUtils.isEmpty(mPhoneticMiddleName) && TextUtils.isEmpty(mPhoneticGivenName) @@ -1381,7 +1425,7 @@ public class VCardEntry { } public String getFullName() { - return mFullName; + return mFormattedName; } public String getPhoneticFamilyName() { diff --git a/core/java/android/pim/vcard/VCardEntryCommitter.java b/core/java/android/pim/vcard/VCardEntryCommitter.java index 59a2baf..a8c8057 100644 --- a/core/java/android/pim/vcard/VCardEntryCommitter.java +++ b/core/java/android/pim/vcard/VCardEntryCommitter.java @@ -52,9 +52,9 @@ public class VCardEntryCommitter implements VCardEntryHandler { } } - public void onEntryCreated(final VCardEntry contactStruct) { + public void onEntryCreated(final VCardEntry vcardEntry) { long start = System.currentTimeMillis(); - mCreatedUris.add(contactStruct.pushIntoContentResolver(mContentResolver)); + mCreatedUris.add(vcardEntry.pushIntoContentResolver(mContentResolver)); mTimeToCommit += System.currentTimeMillis() - start; } diff --git a/core/java/android/pim/vcard/VCardEntryConstructor.java b/core/java/android/pim/vcard/VCardEntryConstructor.java index ae4ec29..aa3e3e2 100644 --- a/core/java/android/pim/vcard/VCardEntryConstructor.java +++ b/core/java/android/pim/vcard/VCardEntryConstructor.java @@ -16,78 +16,82 @@ package android.pim.vcard; import android.accounts.Account; +import android.text.TextUtils; +import android.util.Base64; import android.util.CharsetUtils; import android.util.Log; -import org.apache.commons.codec.DecoderException; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.net.QuotedPrintableCodec; - -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.List; +/** + * <p> + * The {@link VCardInterpreter} implementation which enables {@link VCardEntryHandler} objects + * to easily handle each vCard entry. + * </p> + * <p> + * This class understand details inside vCard and translates it to {@link VCardEntry}. + * Then the class throw it to {@link VCardEntryHandler} registered via + * {@link #addEntryHandler(VCardEntryHandler)}, so that all those registered objects + * are able to handle the {@link VCardEntry} object. + * </p> + * <p> + * If you want to know the detail inside vCard, it would be better to implement + * {@link VCardInterpreter} directly, instead of relying on this class and + * {@link VCardEntry} created by the object. + * </p> + */ public class VCardEntryConstructor implements VCardInterpreter { private static String LOG_TAG = "VCardEntryConstructor"; - /** - * If there's no other information available, this class uses this charset for encoding - * byte arrays to String. - */ - /* package */ static final String DEFAULT_CHARSET_FOR_DECODED_BYTES = "UTF-8"; - private VCardEntry.Property mCurrentProperty = new VCardEntry.Property(); - private VCardEntry mCurrentContactStruct; + private VCardEntry mCurrentVCardEntry; private String mParamType; - /** - * The charset using which {@link VCardInterpreter} parses the text. - */ - private String mInputCharset; - - /** - * The charset with which byte array is encoded to String. - */ - final private String mCharsetForDecodedBytes; - final private boolean mStrictLineBreakParsing; - final private int mVCardType; - final private Account mAccount; + // The charset using which {@link VCardInterpreter} parses the text. + // Each String is first decoded into binary stream with this charset, and encoded back + // to "target charset", which may be explicitly specified by the vCard with "CHARSET" + // property or implicitly mentioned by its version (e.g. vCard 3.0 recommends UTF-8). + private final String mSourceCharset; + + private final boolean mStrictLineBreaking; + private final int mVCardType; + private final Account mAccount; - /** For measuring performance. */ + // For measuring performance. private long mTimePushIntoContentResolver; - final private List<VCardEntryHandler> mEntryHandlers = new ArrayList<VCardEntryHandler>(); + private final List<VCardEntryHandler> mEntryHandlers = new ArrayList<VCardEntryHandler>(); public VCardEntryConstructor() { - this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, null); + this(VCardConfig.VCARD_TYPE_V21_GENERIC, null); } public VCardEntryConstructor(final int vcardType) { - this(null, null, false, vcardType, null); + this(vcardType, null, null, false); } - public VCardEntryConstructor(final String charset, final boolean strictLineBreakParsing, - final int vcardType, final Account account) { - this(null, charset, strictLineBreakParsing, vcardType, account); + public VCardEntryConstructor(final int vcardType, final Account account) { + this(vcardType, account, null, false); } - public VCardEntryConstructor(final String inputCharset, final String charsetForDetodedBytes, - final boolean strictLineBreakParsing, final int vcardType, - final Account account) { + public VCardEntryConstructor(final int vcardType, final Account account, + final String inputCharset) { + this(vcardType, account, inputCharset, false); + } + + /** + * @hide Just for testing. + */ + public VCardEntryConstructor(final int vcardType, final Account account, + final String inputCharset, final boolean strictLineBreakParsing) { if (inputCharset != null) { - mInputCharset = inputCharset; - } else { - mInputCharset = VCardConfig.DEFAULT_CHARSET; - } - if (charsetForDetodedBytes != null) { - mCharsetForDecodedBytes = charsetForDetodedBytes; + mSourceCharset = inputCharset; } else { - mCharsetForDecodedBytes = DEFAULT_CHARSET_FOR_DECODED_BYTES; + mSourceCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET; } - mStrictLineBreakParsing = strictLineBreakParsing; + mStrictLineBreaking = strictLineBreakParsing; mVCardType = vcardType; mAccount = account; } @@ -95,60 +99,63 @@ public class VCardEntryConstructor implements VCardInterpreter { public void addEntryHandler(VCardEntryHandler entryHandler) { mEntryHandlers.add(entryHandler); } - + + @Override public void start() { for (VCardEntryHandler entryHandler : mEntryHandlers) { entryHandler.onStart(); } } + @Override public void end() { for (VCardEntryHandler entryHandler : mEntryHandlers) { entryHandler.onEnd(); } } - /** - * Called when the parse failed between {@link #startEntry()} and {@link #endEntry()}. - */ public void clear() { - mCurrentContactStruct = null; + mCurrentVCardEntry = null; mCurrentProperty = new VCardEntry.Property(); } - /** - * Assume that VCard is not nested. In other words, this code does not accept - */ + @Override public void startEntry() { - if (mCurrentContactStruct != null) { + if (mCurrentVCardEntry != null) { Log.e(LOG_TAG, "Nested VCard code is not supported now."); } - mCurrentContactStruct = new VCardEntry(mVCardType, mAccount); + mCurrentVCardEntry = new VCardEntry(mVCardType, mAccount); } + @Override public void endEntry() { - mCurrentContactStruct.consolidateFields(); + mCurrentVCardEntry.consolidateFields(); for (VCardEntryHandler entryHandler : mEntryHandlers) { - entryHandler.onEntryCreated(mCurrentContactStruct); + entryHandler.onEntryCreated(mCurrentVCardEntry); } - mCurrentContactStruct = null; + mCurrentVCardEntry = null; } + @Override public void startProperty() { mCurrentProperty.clear(); } + @Override public void endProperty() { - mCurrentContactStruct.addProperty(mCurrentProperty); + mCurrentVCardEntry.addProperty(mCurrentProperty); } + @Override public void propertyName(String name) { mCurrentProperty.setPropertyName(name); } + @Override public void propertyGroup(String group) { } + @Override public void propertyParamType(String type) { if (mParamType != null) { Log.e(LOG_TAG, "propertyParamType() is called more than once " + @@ -164,119 +171,34 @@ public class VCardEntryConstructor implements VCardInterpreter { mParamType = "TYPE"; } if (!VCardUtils.containsOnlyAlphaDigitHyphen(value)) { - value = encodeString(value, mCharsetForDecodedBytes); + value = VCardUtils.convertStringCharset( + value, mSourceCharset, VCardConfig.DEFAULT_IMPORT_CHARSET); } mCurrentProperty.addParameter(mParamType, value); mParamType = null; } - private String encodeString(String originalString, String charsetForDecodedBytes) { - if (mInputCharset.equalsIgnoreCase(charsetForDecodedBytes)) { - return originalString; - } - Charset charset = Charset.forName(mInputCharset); - ByteBuffer byteBuffer = charset.encode(originalString); - // byteBuffer.array() "may" return byte array which is larger than - // byteBuffer.remaining(). Here, we keep on the safe side. - byte[] bytes = new byte[byteBuffer.remaining()]; - byteBuffer.get(bytes); - try { - return new String(bytes, charsetForDecodedBytes); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes); - return null; + private String handleOneValue(String value, + String sourceCharset, String targetCharset, String encoding) { + // It is possible when some of multiple values are empty. + // e.g. N:;a;;; -> values are "", "a", "", "", and "". + if (TextUtils.isEmpty(value)) { + return ""; } - } - private String handleOneValue(String value, String charsetForDecodedBytes, String encoding) { if (encoding != null) { if (encoding.equals("BASE64") || encoding.equals("B")) { - mCurrentProperty.setPropertyBytes(Base64.decodeBase64(value.getBytes())); + mCurrentProperty.setPropertyBytes(Base64.decode(value.getBytes(), Base64.DEFAULT)); return value; } else if (encoding.equals("QUOTED-PRINTABLE")) { - // "= " -> " ", "=\t" -> "\t". - // Previous code had done this replacement. Keep on the safe side. - StringBuilder builder = new StringBuilder(); - int length = value.length(); - for (int i = 0; i < length; i++) { - char ch = value.charAt(i); - if (ch == '=' && i < length - 1) { - char nextCh = value.charAt(i + 1); - if (nextCh == ' ' || nextCh == '\t') { - - builder.append(nextCh); - i++; - continue; - } - } - builder.append(ch); - } - String quotedPrintable = builder.toString(); - - String[] lines; - if (mStrictLineBreakParsing) { - lines = quotedPrintable.split("\r\n"); - } else { - builder = new StringBuilder(); - length = quotedPrintable.length(); - ArrayList<String> list = new ArrayList<String>(); - for (int i = 0; i < length; i++) { - char ch = quotedPrintable.charAt(i); - if (ch == '\n') { - list.add(builder.toString()); - builder = new StringBuilder(); - } else if (ch == '\r') { - list.add(builder.toString()); - builder = new StringBuilder(); - if (i < length - 1) { - char nextCh = quotedPrintable.charAt(i + 1); - if (nextCh == '\n') { - i++; - } - } - } else { - builder.append(ch); - } - } - String finalLine = builder.toString(); - if (finalLine.length() > 0) { - list.add(finalLine); - } - lines = list.toArray(new String[0]); - } - - builder = new StringBuilder(); - for (String line : lines) { - if (line.endsWith("=")) { - line = line.substring(0, line.length() - 1); - } - builder.append(line); - } - byte[] bytes; - try { - bytes = builder.toString().getBytes(mInputCharset); - } catch (UnsupportedEncodingException e1) { - Log.e(LOG_TAG, "Failed to encode: charset=" + mInputCharset); - bytes = builder.toString().getBytes(); - } - - try { - bytes = QuotedPrintableCodec.decodeQuotedPrintable(bytes); - } catch (DecoderException e) { - Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e); - return ""; - } - - try { - return new String(bytes, charsetForDecodedBytes); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes); - return new String(bytes); - } + return VCardUtils.parseQuotedPrintable( + value, mStrictLineBreaking, sourceCharset, targetCharset); } - // Unknown encoding. Fall back to default. + Log.w(LOG_TAG, "Unknown encoding. Fall back to default."); } - return encodeString(value, charsetForDecodedBytes); + + // Just translate the charset of a given String from inputCharset to a system one. + return VCardUtils.convertStringCharset(value, sourceCharset, targetCharset); } public void propertyValues(List<String> values) { @@ -284,24 +206,27 @@ public class VCardEntryConstructor implements VCardInterpreter { return; } - final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET"); - final String charset = - ((charsetCollection != null) ? charsetCollection.iterator().next() : null); - final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING"); + final Collection<String> charsetCollection = + mCurrentProperty.getParameters(VCardConstants.PARAM_CHARSET); + final Collection<String> encodingCollection = + mCurrentProperty.getParameters(VCardConstants.PARAM_ENCODING); final String encoding = ((encodingCollection != null) ? encodingCollection.iterator().next() : null); - - String charsetForDecodedBytes = CharsetUtils.nameForDefaultVendor(charset); - if (charsetForDecodedBytes == null || charsetForDecodedBytes.length() == 0) { - charsetForDecodedBytes = mCharsetForDecodedBytes; + String targetCharset = CharsetUtils.nameForDefaultVendor( + ((charsetCollection != null) ? charsetCollection.iterator().next() : null)); + if (TextUtils.isEmpty(targetCharset)) { + targetCharset = VCardConfig.DEFAULT_IMPORT_CHARSET; } for (final String value : values) { mCurrentProperty.addToPropertyValueList( - handleOneValue(value, charsetForDecodedBytes, encoding)); + handleOneValue(value, mSourceCharset, targetCharset, encoding)); } } + /** + * @hide + */ public void showPerformanceInfo() { Log.d(LOG_TAG, "time for insert ContactStruct to database: " + mTimePushIntoContentResolver + " ms"); diff --git a/core/java/android/pim/vcard/VCardEntryHandler.java b/core/java/android/pim/vcard/VCardEntryHandler.java index 83a67fe..56bf69d 100644 --- a/core/java/android/pim/vcard/VCardEntryHandler.java +++ b/core/java/android/pim/vcard/VCardEntryHandler.java @@ -16,8 +16,13 @@ package android.pim.vcard; /** - * The interface called by {@link VCardEntryConstructor}. Useful when you don't want to - * handle detailed information as what {@link VCardParser} provides via {@link VCardInterpreter}. + * <p> + * The interface called by {@link VCardEntryConstructor}. + * </p> + * <p> + * This class is useful when you don't want to know vCard data in detail. If you want to know + * it, it would be better to consider using {@link VCardInterpreter}. + * </p> */ public interface VCardEntryHandler { /** diff --git a/core/java/android/pim/vcard/VCardInterpreter.java b/core/java/android/pim/vcard/VCardInterpreter.java index b5237c0..03704a2 100644 --- a/core/java/android/pim/vcard/VCardInterpreter.java +++ b/core/java/android/pim/vcard/VCardInterpreter.java @@ -20,7 +20,7 @@ import java.util.List; /** * <P> * The interface which should be implemented by the classes which have to analyze each - * vCard entry more minutely than {@link VCardEntry} class analysis. + * vCard entry minutely. * </P> * <P> * Here, there are several terms specific to vCard (and this library). diff --git a/core/java/android/pim/vcard/VCardInterpreterCollection.java b/core/java/android/pim/vcard/VCardInterpreterCollection.java index 99f81f7..77e44a0 100644 --- a/core/java/android/pim/vcard/VCardInterpreterCollection.java +++ b/core/java/android/pim/vcard/VCardInterpreterCollection.java @@ -23,9 +23,9 @@ import java.util.List; * {@link VCardInterpreter} objects and make a user object treat them as one * {@link VCardInterpreter} object. */ -public class VCardInterpreterCollection implements VCardInterpreter { +public final class VCardInterpreterCollection implements VCardInterpreter { private final Collection<VCardInterpreter> mInterpreterCollection; - + public VCardInterpreterCollection(Collection<VCardInterpreter> interpreterCollection) { mInterpreterCollection = interpreterCollection; } diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java index 57c52a6..31b9369 100644 --- a/core/java/android/pim/vcard/VCardParser.java +++ b/core/java/android/pim/vcard/VCardParser.java @@ -20,82 +20,36 @@ import android.pim.vcard.exception.VCardException; import java.io.IOException; import java.io.InputStream; -public abstract class VCardParser { - protected final int mParseType; - protected boolean mCanceled; - - public VCardParser() { - this(VCardConfig.PARSE_TYPE_UNKNOWN); - } - - public VCardParser(int parseType) { - mParseType = parseType; - } - +public interface VCardParser { /** - * <P> - * Parses the given stream and send the VCard data into VCardBuilderBase object. - * </P. - * <P> + * <p> + * Parses the given stream and send the vCard data into VCardBuilderBase object. + * </p>. + * <p> * Note that vCard 2.1 specification allows "CHARSET" parameter, and some career sets * local encoding to it. For example, Japanese phone career uses Shift_JIS, which is - * formally allowed in VCard 2.1, but not recommended in VCard 3.0. In VCard 2.1, - * In some exreme case, some VCard may have different charsets in one VCard (though - * we do not see any device which emits such kind of malicious data) - * </P> - * <P> - * In order to avoid "misunderstanding" charset as much as possible, this method - * use "ISO-8859-1" for reading the stream. When charset is specified in some property - * (with "CHARSET=..." parameter), the string is decoded to raw bytes and encoded to - * the charset. This method assumes that "ISO-8859-1" has 1 to 1 mapping in all 8bit - * characters, which is not completely sure. In some cases, this "decoding-encoding" - * scheme may fail. To avoid the case, - * </P> - * <P> - * We recommend you to use {@link VCardSourceDetector} and detect which kind of source the - * VCard comes from and explicitly specify a charset using the result. - * </P> + * formally allowed in vCard 2.1, but not allowed in vCard 3.0. In vCard 2.1, + * In some exreme case, it is allowed for vCard to have different charsets in one vCard. + * </p> + * <p> + * We recommend you use {@link VCardSourceDetector} and detect which kind of source the + * vCard comes from and explicitly specify a charset using the result. + * </p> * * @param is The source to parse. * @param interepreter A {@link VCardInterpreter} object which used to construct data. - * @return Returns true for success. Otherwise returns false. - * @throws IOException, VCardException - */ - public abstract boolean parse(InputStream is, VCardInterpreter interepreter) - throws IOException, VCardException; - - /** - * <P> - * The method variants which accept charset. - * </P> - * <P> - * RFC 2426 "recommends" (not forces) to use UTF-8, so it may be OK to use - * UTF-8 as an encoding when parsing vCard 3.0. But note that some Japanese - * phone uses Shift_JIS as a charset (e.g. W61SH), and another uses - * "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification (e.g. W53K). - * </P> - * - * @param is The source to parse. - * @param charset Charset to be used. - * @param builder The VCardBuilderBase object. - * @return Returns true when successful. Otherwise returns false. * @throws IOException, VCardException */ - public abstract boolean parse(InputStream is, String charset, VCardInterpreter builder) + public void parse(InputStream is, VCardInterpreter interepreter) throws IOException, VCardException; - - /** - * The method variants which tells this object the operation is already canceled. - */ - public abstract void parse(InputStream is, String charset, - VCardInterpreter builder, boolean canceled) - throws IOException, VCardException; - + /** - * Cancel parsing. - * Actual cancel is done after the end of the current one vcard entry parsing. + * <p> + * Cancel parsing vCard. Useful when you want to stop the parse in the other threads. + * </p> + * <p> + * Actual cancel is done after parsing the current vcard. + * </p> */ - public void cancel() { - mCanceled = true; - } + public abstract void cancel(); } diff --git a/core/java/android/pim/vcard/VCardParserImpl_V21.java b/core/java/android/pim/vcard/VCardParserImpl_V21.java new file mode 100644 index 0000000..8b365eb --- /dev/null +++ b/core/java/android/pim/vcard/VCardParserImpl_V21.java @@ -0,0 +1,1046 @@ +/* + * Copyright (C) 2010 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. + */ +package android.pim.vcard; + +import android.pim.vcard.exception.VCardAgentNotSupportedException; +import android.pim.vcard.exception.VCardException; +import android.pim.vcard.exception.VCardInvalidCommentLineException; +import android.pim.vcard.exception.VCardInvalidLineException; +import android.pim.vcard.exception.VCardNestedException; +import android.pim.vcard.exception.VCardVersionException; +import android.text.TextUtils; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * <p> + * Basic implementation achieving vCard parsing. Based on vCard 2.1, + * </p> + * @hide + */ +/* package */ class VCardParserImpl_V21 { + private static final String LOG_TAG = "VCardParserImpl_V21"; + + private static final class EmptyInterpreter implements VCardInterpreter { + @Override + public void end() { + } + @Override + public void endEntry() { + } + @Override + public void endProperty() { + } + @Override + public void propertyGroup(String group) { + } + @Override + public void propertyName(String name) { + } + @Override + public void propertyParamType(String type) { + } + @Override + public void propertyParamValue(String value) { + } + @Override + public void propertyValues(List<String> values) { + } + @Override + public void start() { + } + @Override + public void startEntry() { + } + @Override + public void startProperty() { + } + } + + protected static final class CustomBufferedReader extends BufferedReader { + private long mTime; + + /** + * Needed since "next line" may be null due to end of line. + */ + private boolean mNextLineIsValid; + private String mNextLine; + + public CustomBufferedReader(Reader in) { + super(in); + } + + @Override + public String readLine() throws IOException { + if (mNextLineIsValid) { + final String ret = mNextLine; + mNextLine = null; + mNextLineIsValid = false; + return ret; + } + + long start = System.currentTimeMillis(); + final String line = super.readLine(); + long end = System.currentTimeMillis(); + mTime += end - start; + return line; + } + + /** + * Read one line, but make this object store it in its queue. + */ + public String peekLine() throws IOException { + if (!mNextLineIsValid) { + long start = System.currentTimeMillis(); + final String line = super.readLine(); + long end = System.currentTimeMillis(); + mTime += end - start; + + mNextLine = line; + mNextLineIsValid = true; + } + + return mNextLine; + } + + public long getTotalmillisecond() { + return mTime; + } + } + + private static final String DEFAULT_ENCODING = "8BIT"; + + protected boolean mCanceled; + protected VCardInterpreter mInterpreter; + + protected final String mIntermediateCharset; + + /** + * <p> + * The encoding type for deconding byte streams. This member variable is + * reset to a default encoding every time when a new item comes. + * </p> + * <p> + * "Encoding" in vCard is different from "Charset". It is mainly used for + * addresses, notes, images. "7BIT", "8BIT", "BASE64", and + * "QUOTED-PRINTABLE" are known examples. + * </p> + */ + protected String mCurrentEncoding; + + /** + * <p> + * The reader object to be used internally. + * </p> + * <p> + * Developers should not directly read a line from this object. Use + * getLine() unless there some reason. + * </p> + */ + protected CustomBufferedReader mReader; + + /** + * <p> + * Set for storing unkonwn TYPE attributes, which is not acceptable in vCard + * specification, but happens to be seen in real world vCard. + * </p> + */ + protected final Set<String> mUnknownTypeSet = new HashSet<String>(); + + /** + * <p> + * Set for storing unkonwn VALUE attributes, which is not acceptable in + * vCard specification, but happens to be seen in real world vCard. + * </p> + */ + protected final Set<String> mUnknownValueSet = new HashSet<String>(); + + + // In some cases, vCard is nested. Currently, we only consider the most + // interior vCard data. + // See v21_foma_1.vcf in test directory for more information. + // TODO: Don't ignore by using count, but read all of information outside vCard. + private int mNestCount; + + // Used only for parsing END:VCARD. + private String mPreviousLine; + + // For measuring performance. + private long mTimeTotal; + private long mTimeReadStartRecord; + private long mTimeReadEndRecord; + private long mTimeStartProperty; + private long mTimeEndProperty; + private long mTimeParseItems; + private long mTimeParseLineAndHandleGroup; + private long mTimeParsePropertyValues; + private long mTimeParseAdrOrgN; + private long mTimeHandleMiscPropertyValue; + private long mTimeHandleQuotedPrintable; + private long mTimeHandleBase64; + + public VCardParserImpl_V21() { + this(VCardConfig.VCARD_TYPE_DEFAULT); + } + + public VCardParserImpl_V21(int vcardType) { + if ((vcardType & VCardConfig.FLAG_TORELATE_NEST) != 0) { + mNestCount = 1; + } + + mIntermediateCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET; + } + + /** + * <p> + * Parses the file at the given position. + * </p> + */ + // <pre class="prettyprint">vcard_file = [wsls] vcard [wsls]</pre> + protected void parseVCardFile() throws IOException, VCardException { + boolean readingFirstFile = true; + while (true) { + if (mCanceled) { + break; + } + if (!parseOneVCard(readingFirstFile)) { + break; + } + readingFirstFile = false; + } + + if (mNestCount > 0) { + boolean useCache = true; + for (int i = 0; i < mNestCount; i++) { + readEndVCard(useCache, true); + useCache = false; + } + } + } + + /** + * @return true when a given property name is a valid property name. + */ + protected boolean isValidPropertyName(final String propertyName) { + if (!(getKnownPropertyNameSet().contains(propertyName.toUpperCase()) || + propertyName.startsWith("X-")) + && !mUnknownTypeSet.contains(propertyName)) { + mUnknownTypeSet.add(propertyName); + Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); + } + return true; + } + + /** + * @return String. It may be null, or its length may be 0 + * @throws IOException + */ + protected String getLine() throws IOException { + return mReader.readLine(); + } + + protected String peekLine() throws IOException { + return mReader.peekLine(); + } + + /** + * @return String with it's length > 0 + * @throws IOException + * @throws VCardException when the stream reached end of line + */ + protected String getNonEmptyLine() throws IOException, VCardException { + String line; + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("Reached end of buffer."); + } else if (line.trim().length() > 0) { + return line; + } + } + } + + /* + * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF + * items *CRLF + * "END" [ws] ":" [ws] "VCARD" + */ + private boolean parseOneVCard(boolean firstRead) throws IOException, VCardException { + boolean allowGarbage = false; + if (firstRead) { + if (mNestCount > 0) { + for (int i = 0; i < mNestCount; i++) { + if (!readBeginVCard(allowGarbage)) { + return false; + } + allowGarbage = true; + } + } + } + + if (!readBeginVCard(allowGarbage)) { + return false; + } + final long beforeStartEntry = System.currentTimeMillis(); + mInterpreter.startEntry(); + mTimeReadStartRecord += System.currentTimeMillis() - beforeStartEntry; + + final long beforeParseItems = System.currentTimeMillis(); + parseItems(); + mTimeParseItems += System.currentTimeMillis() - beforeParseItems; + + readEndVCard(true, false); + + final long beforeEndEntry = System.currentTimeMillis(); + mInterpreter.endEntry(); + mTimeReadEndRecord += System.currentTimeMillis() - beforeEndEntry; + return true; + } + + /** + * @return True when successful. False when reaching the end of line + * @throws IOException + * @throws VCardException + */ + protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { + String line; + do { + while (true) { + line = getLine(); + if (line == null) { + return false; + } else if (line.trim().length() > 0) { + break; + } + } + final String[] strArray = line.split(":", 2); + final int length = strArray.length; + + // Although vCard 2.1/3.0 specification does not allow lower cases, + // we found vCard file emitted by some external vCard expoter have such + // invalid Strings. + // So we allow it. + // e.g. + // BEGIN:vCard + if (length == 2 && strArray[0].trim().equalsIgnoreCase("BEGIN") + && strArray[1].trim().equalsIgnoreCase("VCARD")) { + return true; + } else if (!allowGarbage) { + if (mNestCount > 0) { + mPreviousLine = line; + return false; + } else { + throw new VCardException("Expected String \"BEGIN:VCARD\" did not come " + + "(Instead, \"" + line + "\" came)"); + } + } + } while (allowGarbage); + + throw new VCardException("Reached where must not be reached."); + } + + /** + * <p> + * The arguments useCache and allowGarbase are usually true and false + * accordingly when this function is called outside this function itself. + * </p> + * + * @param useCache When true, line is obtained from mPreviousline. + * Otherwise, getLine() is used. + * @param allowGarbage When true, ignore non "END:VCARD" line. + * @throws IOException + * @throws VCardException + */ + protected void readEndVCard(boolean useCache, boolean allowGarbage) throws IOException, + VCardException { + String line; + do { + if (useCache) { + // Though vCard specification does not allow lower cases, + // some data may have them, so we allow it. + line = mPreviousLine; + } else { + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("Expected END:VCARD was not found."); + } else if (line.trim().length() > 0) { + break; + } + } + } + + String[] strArray = line.split(":", 2); + if (strArray.length == 2 && strArray[0].trim().equalsIgnoreCase("END") + && strArray[1].trim().equalsIgnoreCase("VCARD")) { + return; + } else if (!allowGarbage) { + throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); + } + useCache = false; + } while (allowGarbage); + } + + /* + * items = *CRLF item / item + */ + protected void parseItems() throws IOException, VCardException { + boolean ended = false; + + final long beforeBeginProperty = System.currentTimeMillis(); + mInterpreter.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - beforeBeginProperty; + ended = parseItem(); + if (!ended) { + final long beforeEndProperty = System.currentTimeMillis(); + mInterpreter.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - beforeEndProperty; + } + + while (!ended) { + final long beforeStartProperty = System.currentTimeMillis(); + mInterpreter.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - beforeStartProperty; + try { + ended = parseItem(); + } catch (VCardInvalidCommentLineException e) { + Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); + ended = false; + } + + if (!ended) { + final long beforeEndProperty = System.currentTimeMillis(); + mInterpreter.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - beforeEndProperty; + } + } + } + + /* + * item = [groups "."] name [params] ":" value CRLF / [groups "."] "ADR" + * [params] ":" addressparts CRLF / [groups "."] "ORG" [params] ":" orgparts + * CRLF / [groups "."] "N" [params] ":" nameparts CRLF / [groups "."] + * "AGENT" [params] ":" vcard CRLF + */ + protected boolean parseItem() throws IOException, VCardException { + mCurrentEncoding = DEFAULT_ENCODING; + + final String line = getNonEmptyLine(); + long start = System.currentTimeMillis(); + + String[] propertyNameAndValue = separateLineAndHandleGroup(line); + if (propertyNameAndValue == null) { + return true; + } + if (propertyNameAndValue.length != 2) { + throw new VCardInvalidLineException("Invalid line \"" + line + "\""); + } + String propertyName = propertyNameAndValue[0].toUpperCase(); + String propertyValue = propertyNameAndValue[1]; + + mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; + + if (propertyName.equals("ADR") || propertyName.equals("ORG") || propertyName.equals("N")) { + start = System.currentTimeMillis(); + handleMultiplePropertyValue(propertyName, propertyValue); + mTimeParseAdrOrgN += System.currentTimeMillis() - start; + return false; + } else if (propertyName.equals("AGENT")) { + handleAgent(propertyValue); + return false; + } else if (isValidPropertyName(propertyName)) { + if (propertyName.equals("BEGIN")) { + if (propertyValue.equals("VCARD")) { + throw new VCardNestedException("This vCard has nested vCard data in it."); + } else { + throw new VCardException("Unknown BEGIN type: " + propertyValue); + } + } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersionString())) { + throw new VCardVersionException("Incompatible version: " + propertyValue + " != " + + getVersionString()); + } + start = System.currentTimeMillis(); + handlePropertyValue(propertyName, propertyValue); + mTimeParsePropertyValues += System.currentTimeMillis() - start; + return false; + } + + throw new VCardException("Unknown property name: \"" + propertyName + "\""); + } + + // For performance reason, the states for group and property name are merged into one. + static private final int STATE_GROUP_OR_PROPERTY_NAME = 0; + static private final int STATE_PARAMS = 1; + // vCard 3.0 specification allows double-quoted parameters, while vCard 2.1 does not. + static private final int STATE_PARAMS_IN_DQUOTE = 2; + + protected String[] separateLineAndHandleGroup(String line) throws VCardException { + final String[] propertyNameAndValue = new String[2]; + final int length = line.length(); + if (length > 0 && line.charAt(0) == '#') { + throw new VCardInvalidCommentLineException(); + } + + int state = STATE_GROUP_OR_PROPERTY_NAME; + int nameIndex = 0; + + // This loop is developed so that we don't have to take care of bottle neck here. + // Refactor carefully when you need to do so. + for (int i = 0; i < length; i++) { + final char ch = line.charAt(i); + switch (state) { + case STATE_GROUP_OR_PROPERTY_NAME: { + if (ch == ':') { // End of a property name. + final String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + mInterpreter.propertyName(propertyName); + propertyNameAndValue[0] = propertyName; + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } else if (ch == '.') { // Each group is followed by the dot. + final String groupName = line.substring(nameIndex, i); + if (groupName.length() == 0) { + Log.w(LOG_TAG, "Empty group found. Ignoring."); + } else { + mInterpreter.propertyGroup(groupName); + } + nameIndex = i + 1; // Next should be another group or a property name. + } else if (ch == ';') { // End of property name and beginneng of parameters. + final String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + mInterpreter.propertyName(propertyName); + propertyNameAndValue[0] = propertyName; + nameIndex = i + 1; + state = STATE_PARAMS; // Start parameter parsing. + } + // TODO: comma support (in vCard 3.0 and 4.0). + break; + } + case STATE_PARAMS: { + if (ch == '"') { + if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) { + Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " + + "Silently allow it"); + } + state = STATE_PARAMS_IN_DQUOTE; + } else if (ch == ';') { // Starts another param. + handleParams(line.substring(nameIndex, i)); + nameIndex = i + 1; + } else if (ch == ':') { // End of param and beginenning of values. + handleParams(line.substring(nameIndex, i)); + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } + break; + } + case STATE_PARAMS_IN_DQUOTE: { + if (ch == '"') { + if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) { + Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " + + "Silently allow it"); + } + state = STATE_PARAMS; + } + break; + } + } + } + + throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); + } + + /* + * params = ";" [ws] paramlist paramlist = paramlist [ws] ";" [ws] param / + * param param = "TYPE" [ws] "=" [ws] ptypeval / "VALUE" [ws] "=" [ws] + * pvalueval / "ENCODING" [ws] "=" [ws] pencodingval / "CHARSET" [ws] "=" + * [ws] charsetval / "LANGUAGE" [ws] "=" [ws] langval / "X-" word [ws] "=" + * [ws] word / knowntype + */ + protected void handleParams(String params) throws VCardException { + final String[] strArray = params.split("=", 2); + if (strArray.length == 2) { + final String paramName = strArray[0].trim().toUpperCase(); + String paramValue = strArray[1].trim(); + if (paramName.equals("TYPE")) { + handleType(paramValue); + } else if (paramName.equals("VALUE")) { + handleValue(paramValue); + } else if (paramName.equals("ENCODING")) { + handleEncoding(paramValue); + } else if (paramName.equals("CHARSET")) { + handleCharset(paramValue); + } else if (paramName.equals("LANGUAGE")) { + handleLanguage(paramValue); + } else if (paramName.startsWith("X-")) { + handleAnyParam(paramName, paramValue); + } else { + throw new VCardException("Unknown type \"" + paramName + "\""); + } + } else { + handleParamWithoutName(strArray[0]); + } + } + + /** + * vCard 3.0 parser implementation may throw VCardException. + */ + @SuppressWarnings("unused") + protected void handleParamWithoutName(final String paramValue) throws VCardException { + handleType(paramValue); + } + + /* + * ptypeval = knowntype / "X-" word + */ + protected void handleType(final String ptypeval) { + if (!(getKnownTypeSet().contains(ptypeval.toUpperCase()) + || ptypeval.startsWith("X-")) + && !mUnknownTypeSet.contains(ptypeval)) { + mUnknownTypeSet.add(ptypeval); + Log.w(LOG_TAG, String.format("TYPE unsupported by %s: ", getVersion(), ptypeval)); + } + mInterpreter.propertyParamType("TYPE"); + mInterpreter.propertyParamValue(ptypeval); + } + + /* + * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word + */ + protected void handleValue(final String pvalueval) { + if (!(getKnownValueSet().contains(pvalueval.toUpperCase()) + || pvalueval.startsWith("X-") + || mUnknownValueSet.contains(pvalueval))) { + mUnknownValueSet.add(pvalueval); + Log.w(LOG_TAG, String.format( + "The value unsupported by TYPE of %s: ", getVersion(), pvalueval)); + } + mInterpreter.propertyParamType("VALUE"); + mInterpreter.propertyParamValue(pvalueval); + } + + /* + * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word + */ + protected void handleEncoding(String pencodingval) throws VCardException { + if (getAvailableEncodingSet().contains(pencodingval) || + pencodingval.startsWith("X-")) { + mInterpreter.propertyParamType("ENCODING"); + mInterpreter.propertyParamValue(pencodingval); + mCurrentEncoding = pencodingval; + } else { + throw new VCardException("Unknown encoding \"" + pencodingval + "\""); + } + } + + /** + * <p> + * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521), + * but recent vCard files often contain other charset like UTF-8, SHIFT_JIS, etc. + * We allow any charset. + * </p> + */ + protected void handleCharset(String charsetval) { + mInterpreter.propertyParamType("CHARSET"); + mInterpreter.propertyParamValue(charsetval); + } + + /** + * See also Section 7.1 of RFC 1521 + */ + protected void handleLanguage(String langval) throws VCardException { + String[] strArray = langval.split("-"); + if (strArray.length != 2) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + String tmp = strArray[0]; + int length = tmp.length(); + for (int i = 0; i < length; i++) { + if (!isAsciiLetter(tmp.charAt(i))) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + } + tmp = strArray[1]; + length = tmp.length(); + for (int i = 0; i < length; i++) { + if (!isAsciiLetter(tmp.charAt(i))) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + } + mInterpreter.propertyParamType(VCardConstants.PARAM_LANGUAGE); + mInterpreter.propertyParamValue(langval); + } + + private boolean isAsciiLetter(char ch) { + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { + return true; + } + return false; + } + + /** + * Mainly for "X-" type. This accepts any kind of type without check. + */ + protected void handleAnyParam(String paramName, String paramValue) { + mInterpreter.propertyParamType(paramName); + mInterpreter.propertyParamValue(paramValue); + } + + protected void handlePropertyValue(String propertyName, String propertyValue) + throws IOException, VCardException { + final String upperEncoding = mCurrentEncoding.toUpperCase(); + if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_QP)) { + final long start = System.currentTimeMillis(); + final String result = getQuotedPrintable(propertyValue); + final ArrayList<String> v = new ArrayList<String>(); + v.add(result); + mInterpreter.propertyValues(v); + mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; + } else if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_BASE64) + || upperEncoding.equals(VCardConstants.PARAM_ENCODING_B)) { + final long start = System.currentTimeMillis(); + // It is very rare, but some BASE64 data may be so big that + // OutOfMemoryError occurs. To ignore such cases, use try-catch. + try { + final ArrayList<String> arrayList = new ArrayList<String>(); + arrayList.add(getBase64(propertyValue)); + mInterpreter.propertyValues(arrayList); + } catch (OutOfMemoryError error) { + Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); + mInterpreter.propertyValues(null); + } + mTimeHandleBase64 += System.currentTimeMillis() - start; + } else { + if (!(upperEncoding.equals("7BIT") || upperEncoding.equals("8BIT") || + upperEncoding.startsWith("X-"))) { + Log.w(LOG_TAG, + String.format("The encoding \"%s\" is unsupported by vCard %s", + mCurrentEncoding, getVersionString())); + } + + // Some device uses line folding defined in RFC 2425, which is not allowed + // in vCard 2.1 (while needed in vCard 3.0). + // + // e.g. + // BEGIN:VCARD + // VERSION:2.1 + // N:;Omega;;; + // EMAIL;INTERNET:"Omega" + // <omega@example.com> + // FN:Omega + // END:VCARD + // + // The vCard above assumes that email address should become: + // "Omega" <omega@example.com> + // + // But vCard 2.1 requires Quote-Printable when a line contains line break(s). + // + // For more information about line folding, + // see "5.8.1. Line delimiting and folding" in RFC 2425. + // + // We take care of this case more formally in vCard 3.0, so we only need to + // do this in vCard 2.1. + if (getVersion() == VCardConfig.VERSION_21) { + StringBuilder builder = null; + while (true) { + final String nextLine = peekLine(); + // We don't need to care too much about this exceptional case, + // but we should not wrongly eat up "END:VCARD", since it critically + // breaks this parser's state machine. + // Thus we roughly look over the next line and confirm it is at least not + // "END:VCARD". This extra fee is worth paying. This is exceptional + // anyway. + if (!TextUtils.isEmpty(nextLine) && + nextLine.charAt(0) == ' ' && + !"END:VCARD".contains(nextLine.toUpperCase())) { + getLine(); // Drop the next line. + + if (builder == null) { + builder = new StringBuilder(); + builder.append(propertyValue); + } + builder.append(nextLine.substring(1)); + } else { + break; + } + } + if (builder != null) { + propertyValue = builder.toString(); + } + } + + final long start = System.currentTimeMillis(); + ArrayList<String> v = new ArrayList<String>(); + v.add(maybeUnescapeText(propertyValue)); + mInterpreter.propertyValues(v); + mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; + } + } + + /** + * <p> + * Parses and returns Quoted-Printable. + * </p> + * + * @param firstString The string following a parameter name and attributes. + * Example: "string" in + * "ADR:ENCODING=QUOTED-PRINTABLE:string\n\r". + * @return whole Quoted-Printable string, including a given argument and + * following lines. Excludes the last empty line following to Quoted + * Printable lines. + * @throws IOException + * @throws VCardException + */ + private String getQuotedPrintable(String firstString) throws IOException, VCardException { + // Specifically, there may be some padding between = and CRLF. + // See the following: + // + // qp-line := *(qp-segment transport-padding CRLF) + // qp-part transport-padding + // qp-segment := qp-section *(SPACE / TAB) "=" + // ; Maximum length of 76 characters + // + // e.g. (from RFC 2045) + // Now's the time = + // for all folk to come= + // to the aid of their country. + if (firstString.trim().endsWith("=")) { + // remove "transport-padding" + int pos = firstString.length() - 1; + while (firstString.charAt(pos) != '=') { + } + StringBuilder builder = new StringBuilder(); + builder.append(firstString.substring(0, pos + 1)); + builder.append("\r\n"); + String line; + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("File ended during parsing a Quoted-Printable String"); + } + if (line.trim().endsWith("=")) { + // remove "transport-padding" + pos = line.length() - 1; + while (line.charAt(pos) != '=') { + } + builder.append(line.substring(0, pos + 1)); + builder.append("\r\n"); + } else { + builder.append(line); + break; + } + } + return builder.toString(); + } else { + return firstString; + } + } + + protected String getBase64(String firstString) throws IOException, VCardException { + StringBuilder builder = new StringBuilder(); + builder.append(firstString); + + while (true) { + String line = getLine(); + if (line == null) { + throw new VCardException("File ended during parsing BASE64 binary"); + } + if (line.length() == 0) { + break; + } + builder.append(line); + } + + return builder.toString(); + } + + /** + * <p> + * Mainly for "ADR", "ORG", and "N" + * </p> + */ + /* + * addressparts = 0*6(strnosemi ";") strnosemi ; PO Box, Extended Addr, + * Street, Locality, Region, Postal Code, Country Name orgparts = + * *(strnosemi ";") strnosemi ; First is Organization Name, remainder are + * Organization Units. nameparts = 0*4(strnosemi ";") strnosemi ; Family, + * Given, Middle, Prefix, Suffix. ; Example:Public;John;Q.;Reverend Dr.;III, + * Esq. strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi ; To include a + * semicolon in this string, it must be escaped ; with a "\" character. We + * do not care the number of "strnosemi" here. We are not sure whether we + * should add "\" CRLF to each value. We exclude them for now. + */ + protected void handleMultiplePropertyValue(String propertyName, String propertyValue) + throws IOException, VCardException { + // vCard 2.1 does not allow QUOTED-PRINTABLE here, but some + // softwares/devices + // emit such data. + if (mCurrentEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { + propertyValue = getQuotedPrintable(propertyValue); + } + + mInterpreter.propertyValues(VCardUtils.constructListFromValue(propertyValue, + getVersion())); + } + + /* + * vCard 2.1 specifies AGENT allows one vcard entry. Currently we emit an + * error toward the AGENT property. + * // TODO: Support AGENT property. + * item = + * ... / [groups "."] "AGENT" [params] ":" vcard CRLF vcard = "BEGIN" [ws] + * ":" [ws] "VCARD" [ws] 1*CRLF items *CRLF "END" [ws] ":" [ws] "VCARD" + */ + protected void handleAgent(final String propertyValue) throws VCardException { + if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) { + // Apparently invalid line seen in Windows Mobile 6.5. Ignore them. + return; + } else { + throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); + } + } + + /** + * For vCard 3.0. + */ + protected String maybeUnescapeText(final String text) { + return text; + } + + /** + * Returns unescaped String if the character should be unescaped. Return + * null otherwise. e.g. In vCard 2.1, "\;" should be unescaped into ";" + * while "\x" should not be. + */ + protected String maybeUnescapeCharacter(final char ch) { + return unescapeCharacter(ch); + } + + /* package */ static String unescapeCharacter(final char ch) { + // Original vCard 2.1 specification does not allow transformation + // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous + // implementation of + // this class allowed them, so keep it as is. + if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { + return String.valueOf(ch); + } else { + return null; + } + } + + private void showPerformanceInfo() { + Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); + Log.d(LOG_TAG, "Total readLine time: " + mReader.getTotalmillisecond() + " ms"); + Log.d(LOG_TAG, "Time for handling the beggining of the record: " + mTimeReadStartRecord + + " ms"); + Log.d(LOG_TAG, "Time for handling the end of the record: " + mTimeReadEndRecord + " ms"); + Log.d(LOG_TAG, "Time for parsing line, and handling group: " + mTimeParseLineAndHandleGroup + + " ms"); + Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); + Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); + Log.d(LOG_TAG, "Time for handling normal property values: " + mTimeHandleMiscPropertyValue + + " ms"); + Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + mTimeHandleQuotedPrintable + " ms"); + Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); + } + + /** + * @return {@link VCardConfig#VERSION_21} + */ + protected int getVersion() { + return VCardConfig.VERSION_21; + } + + /** + * @return {@link VCardConfig#VERSION_30} + */ + protected String getVersionString() { + return VCardConstants.VERSION_V21; + } + + protected Set<String> getKnownPropertyNameSet() { + return VCardParser_V21.sKnownPropertyNameSet; + } + + protected Set<String> getKnownTypeSet() { + return VCardParser_V21.sKnownTypeSet; + } + + protected Set<String> getKnownValueSet() { + return VCardParser_V21.sKnownValueSet; + } + + protected Set<String> getAvailableEncodingSet() { + return VCardParser_V21.sAvailableEncoding; + } + + protected String getDefaultEncoding() { + return DEFAULT_ENCODING; + } + + + public void parse(InputStream is, VCardInterpreter interpreter) + throws IOException, VCardException { + if (is == null) { + throw new NullPointerException("InputStream must not be null."); + } + + final InputStreamReader tmpReader = new InputStreamReader(is, mIntermediateCharset); + mReader = new CustomBufferedReader(tmpReader); + + mInterpreter = (interpreter != null ? interpreter : new EmptyInterpreter()); + + final long start = System.currentTimeMillis(); + if (mInterpreter != null) { + mInterpreter.start(); + } + parseVCardFile(); + if (mInterpreter != null) { + mInterpreter.end(); + } + mTimeTotal += System.currentTimeMillis() - start; + + if (VCardConfig.showPerformanceLog()) { + showPerformanceInfo(); + } + } + + public final void cancel() { + mCanceled = true; + } +} diff --git a/core/java/android/pim/vcard/VCardParserImpl_V30.java b/core/java/android/pim/vcard/VCardParserImpl_V30.java new file mode 100644 index 0000000..3fad9a0 --- /dev/null +++ b/core/java/android/pim/vcard/VCardParserImpl_V30.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2010 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. + */ +package android.pim.vcard; + +import android.pim.vcard.exception.VCardException; +import android.util.Log; + +import java.io.IOException; +import java.util.Set; + +/** + * <p> + * Basic implementation achieving vCard 3.0 parsing. + * </p> + * <p> + * This class inherits vCard 2.1 implementation since technically they are similar, + * while specifically there's logical no relevance between them. + * So that developers are not confused with the inheritance, + * {@link VCardParser_V30} does not inherit {@link VCardParser_V21}, while + * {@link VCardParserImpl_V30} inherits {@link VCardParserImpl_V21}. + * </p> + * @hide + */ +/* package */ class VCardParserImpl_V30 extends VCardParserImpl_V21 { + private static final String LOG_TAG = "VCardParserImpl_V30"; + + private String mPreviousLine; + private boolean mEmittedAgentWarning = false; + + public VCardParserImpl_V30() { + super(); + } + + public VCardParserImpl_V30(int vcardType) { + super(vcardType); + } + + @Override + protected int getVersion() { + return VCardConfig.VERSION_30; + } + + @Override + protected String getVersionString() { + return VCardConstants.VERSION_V30; + } + + @Override + protected String getLine() throws IOException { + if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } else { + return mReader.readLine(); + } + } + + /** + * vCard 3.0 requires that the line with space at the beginning of the line + * must be combined with previous line. + */ + @Override + protected String getNonEmptyLine() throws IOException, VCardException { + String line; + StringBuilder builder = null; + while (true) { + line = mReader.readLine(); + if (line == null) { + if (builder != null) { + return builder.toString(); + } else if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } + throw new VCardException("Reached end of buffer."); + } else if (line.length() == 0) { + if (builder != null) { + return builder.toString(); + } else if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } + } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { + if (builder != null) { + // See Section 5.8.1 of RFC 2425 (MIME-DIR document). + // Following is the excerpts from it. + // + // DESCRIPTION:This is a long description that exists on a long line. + // + // Can be represented as: + // + // DESCRIPTION:This is a long description + // that exists on a long line. + // + // It could also be represented as: + // + // DESCRIPTION:This is a long descrip + // tion that exists o + // n a long line. + builder.append(line.substring(1)); + } else if (mPreviousLine != null) { + builder = new StringBuilder(); + builder.append(mPreviousLine); + mPreviousLine = null; + builder.append(line.substring(1)); + } else { + throw new VCardException("Space exists at the beginning of the line"); + } + } else { + if (mPreviousLine == null) { + mPreviousLine = line; + if (builder != null) { + return builder.toString(); + } + } else { + String ret = mPreviousLine; + mPreviousLine = line; + return ret; + } + } + } + } + + /* + * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF + * 1 * (contentline) + * ;A vCard object MUST include the VERSION, FN and N types. + * [group "."] "END" ":" "VCARD" 1 * CRLF + */ + @Override + protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { + // TODO: vCard 3.0 supports group. + return super.readBeginVCard(allowGarbage); + } + + @Override + protected void readEndVCard(boolean useCache, boolean allowGarbage) + throws IOException, VCardException { + // TODO: vCard 3.0 supports group. + super.readEndVCard(useCache, allowGarbage); + } + + /** + * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. + */ + @Override + protected void handleParams(final String params) throws VCardException { + try { + super.handleParams(params); + } catch (VCardException e) { + // maybe IANA type + String[] strArray = params.split("=", 2); + if (strArray.length == 2) { + handleAnyParam(strArray[0], strArray[1]); + } else { + // Must not come here in the current implementation. + throw new VCardException( + "Unknown params value: " + params); + } + } + } + + @Override + protected void handleAnyParam(final String paramName, final String paramValue) { + mInterpreter.propertyParamType(paramName); + splitAndPutParamValue(paramValue); + } + + @Override + protected void handleParamWithoutName(final String paramValue) { + handleType(paramValue); + } + + /* + * vCard 3.0 defines + * + * param = param-name "=" param-value *("," param-value) + * param-name = iana-token / x-name + * param-value = ptext / quoted-string + * quoted-string = DQUOTE QSAFE-CHAR DQUOTE + * QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-ASCII + * ; Any character except CTLs, DQUOTE + * + * QSAFE-CHAR must not contain DQUOTE, including escaped one (\"). + */ + @Override + protected void handleType(final String paramValue) { + mInterpreter.propertyParamType("TYPE"); + splitAndPutParamValue(paramValue); + } + + /** + * Splits parameter values into pieces in accordance with vCard 3.0 specification and + * puts pieces into mInterpreter. + */ + /* + * param-value = ptext / quoted-string + * quoted-string = DQUOTE QSAFE-CHAR DQUOTE + * QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-ASCII + * ; Any character except CTLs, DQUOTE + * + * QSAFE-CHAR must not contain DQUOTE, including escaped one (\") + */ + private void splitAndPutParamValue(String paramValue) { + // "comma,separated:inside.dquote",pref + // --> + // - comma,separated:inside.dquote + // - pref + // + // Note: Though there's a code, we don't need to take much care of + // wrongly-added quotes like the example above, as they induce + // parse errors at the top level (when splitting a line into parts). + StringBuilder builder = null; // Delay initialization. + boolean insideDquote = false; + final int length = paramValue.length(); + for (int i = 0; i < length; i++) { + final char ch = paramValue.charAt(i); + if (ch == '"') { + if (insideDquote) { + // End of Dquote. + mInterpreter.propertyParamValue(builder.toString()); + builder = null; + insideDquote = false; + } else { + if (builder != null) { + if (builder.length() > 0) { + // e.g. + // pref"quoted" + Log.w(LOG_TAG, "Unexpected Dquote inside property."); + } else { + // e.g. + // pref,"quoted" + // "quoted",pref + mInterpreter.propertyParamValue(builder.toString()); + } + } + insideDquote = true; + } + } else if (ch == ',' && !insideDquote) { + if (builder == null) { + Log.w(LOG_TAG, "Comma is used before actual string comes. (" + + paramValue + ")"); + } else { + mInterpreter.propertyParamValue(builder.toString()); + builder = null; + } + } else { + // To stop creating empty StringBuffer at the end of parameter, + // we delay creating this object until this point. + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(ch); + } + } + if (insideDquote) { + // e.g. + // "non-quote-at-end + Log.d(LOG_TAG, "Dangling Dquote."); + } + if (builder != null) { + if (builder.length() == 0) { + Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " + + "at the end of parameter value parsing."); + } else { + mInterpreter.propertyParamValue(builder.toString()); + } + } + } + + @Override + protected void handleAgent(final String propertyValue) { + // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1. + // + // e.g. + // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n + // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n + // ET:jfriday@host.com\nEND:VCARD\n + // + // TODO: fix this. + // + // issue: + // vCard 3.0 also allows this as an example. + // + // AGENT;VALUE=uri: + // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com + // + // This is not vCard. Should we support this? + // + // Just ignore the line for now, since we cannot know how to handle it... + if (!mEmittedAgentWarning) { + Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); + mEmittedAgentWarning = true; + } + } + + /** + * vCard 3.0 does not require two CRLF at the last of BASE64 data. + * It only requires that data should be MIME-encoded. + */ + @Override + protected String getBase64(final String firstString) + throws IOException, VCardException { + final StringBuilder builder = new StringBuilder(); + builder.append(firstString); + + while (true) { + final String line = getLine(); + if (line == null) { + throw new VCardException("File ended during parsing BASE64 binary"); + } + if (line.length() == 0) { + break; + } else if (!line.startsWith(" ") && !line.startsWith("\t")) { + mPreviousLine = line; + break; + } + builder.append(line); + } + + return builder.toString(); + } + + /** + * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") + * ; \\ encodes \, \n or \N encodes newline + * ; \; encodes ;, \, encodes , + * + * Note: Apple escapes ':' into '\:' while does not escape '\' + */ + @Override + protected String maybeUnescapeText(final String text) { + return unescapeText(text); + } + + public static String unescapeText(final String text) { + StringBuilder builder = new StringBuilder(); + final int length = text.length(); + for (int i = 0; i < length; i++) { + char ch = text.charAt(i); + if (ch == '\\' && i < length - 1) { + final char next_ch = text.charAt(++i); + if (next_ch == 'n' || next_ch == 'N') { + builder.append("\n"); + } else { + builder.append(next_ch); + } + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + @Override + protected String maybeUnescapeCharacter(final char ch) { + return unescapeCharacter(ch); + } + + public static String unescapeCharacter(final char ch) { + if (ch == 'n' || ch == 'N') { + return "\n"; + } else { + return String.valueOf(ch); + } + } + + @Override + protected Set<String> getKnownPropertyNameSet() { + return VCardParser_V30.sKnownPropertyNameSet; + } +} diff --git a/core/java/android/pim/vcard/VCardParserImpl_V40.java b/core/java/android/pim/vcard/VCardParserImpl_V40.java new file mode 100644 index 0000000..0fe76bb --- /dev/null +++ b/core/java/android/pim/vcard/VCardParserImpl_V40.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2010 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. + */ +package android.pim.vcard; + +import java.util.Set; + + +/** + * <p> + * Basic implementation parsing vCard 4.0. + * </p> + * <p> + * vCard 4.0 is not published yet. Also this implementation is premature. + * </p> + * @hide + */ +/* package */ class VCardParserImpl_V40 extends VCardParserImpl_V30 { + // private static final String LOG_TAG = "VCardParserImpl_V40"; + + public VCardParserImpl_V40() { + super(); + } + + public VCardParserImpl_V40(final int vcardType) { + super(vcardType); + } + + @Override + protected int getVersion() { + return VCardConfig.VERSION_40; + } + + @Override + protected String getVersionString() { + return VCardConstants.VERSION_V40; + } + + /** + * We escape "\N" into new line for safety. + */ + @Override + protected String maybeUnescapeText(final String text) { + return unescapeText(text); + } + + public static String unescapeText(final String text) { + // TODO: more strictly, vCard 4.0 requires different type of unescaping rule + // toward each property. + final StringBuilder builder = new StringBuilder(); + final int length = text.length(); + for (int i = 0; i < length; i++) { + char ch = text.charAt(i); + if (ch == '\\' && i < length - 1) { + final char next_ch = text.charAt(++i); + if (next_ch == 'n' || next_ch == 'N') { + builder.append("\n"); + } else { + builder.append(next_ch); + } + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + public static String unescapeCharacter(final char ch) { + if (ch == 'n' || ch == 'N') { + return "\n"; + } else { + return String.valueOf(ch); + } + } + + @Override + protected Set<String> getKnownPropertyNameSet() { + return VCardParser_V40.sKnownPropertyNameSet; + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java index fe8cfb0..507a176 100644 --- a/core/java/android/pim/vcard/VCardParser_V21.java +++ b/core/java/android/pim/vcard/VCardParser_V21.java @@ -15,922 +15,95 @@ */ package android.pim.vcard; -import android.pim.vcard.exception.VCardAgentNotSupportedException; import android.pim.vcard.exception.VCardException; -import android.pim.vcard.exception.VCardInvalidCommentLineException; -import android.pim.vcard.exception.VCardInvalidLineException; -import android.pim.vcard.exception.VCardNestedException; -import android.pim.vcard.exception.VCardVersionException; -import android.util.Log; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Set; /** - * This class is used to parse vCard. Please refer to vCard Specification 2.1 for more detail. + * </p> + * vCard parser for vCard 2.1. See the specification for more detail about the spec itself. + * </p> + * <p> + * The spec is written in 1996, and currently various types of "vCard 2.1" exist. + * To handle real the world vCard formats appropriately and effectively, this class does not + * obey with strict vCard 2.1. + * In stead, not only vCard spec but also real world vCard is considered. + * </p> + * e.g. A lot of devices and softwares let vCard importer/exporter to use + * the PNG format to determine the type of image, while it is not allowed in + * the original specification. As of 2010, we can see even the FLV format + * (possible in Japanese mobile phones). + * </p> */ -public class VCardParser_V21 extends VCardParser { - private static final String LOG_TAG = "VCardParser_V21"; - - /** Store the known-type */ - private static final HashSet<String> sKnownTypeSet = new HashSet<String>( - Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", - "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS", - "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK", - "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL", - "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF", - "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", - "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", - "WAVE", "AIFF", "PCM", "X509", "PGP")); - - /** Store the known-value */ - private static final HashSet<String> sKnownValueSet = new HashSet<String>( - Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); - - /** Store the property names available in vCard 2.1 */ - private static final HashSet<String> sAvailablePropertyNameSetV21 = - new HashSet<String>(Arrays.asList( - "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", - "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", - "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER")); - +public final class VCardParser_V21 implements VCardParser { /** - * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. - * We allow it for safety... - */ - private static final HashSet<String> sAvailableEncodingV21 = - new HashSet<String>(Arrays.asList( - "7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B")); - - // Used only for parsing END:VCARD. - private String mPreviousLine; - - /** The builder to build parsed data */ - protected VCardInterpreter mBuilder = null; - - /** - * The encoding type. "Encoding" in vCard is different from "Charset". - * e.g. 7BIT, 8BIT, QUOTED-PRINTABLE. + * A unmodifiable Set storing the property names available in the vCard 2.1 specification. */ - protected String mEncoding = null; - - protected final String sDefaultEncoding = "8BIT"; - - // Should not directly read a line from this object. Use getLine() instead. - protected BufferedReader mReader; - - // In some cases, vCard is nested. Currently, we only consider the most interior vCard data. - // See v21_foma_1.vcf in test directory for more information. - private int mNestCount; - - // In order to reduce warning message as much as possible, we hold the value which made Logger - // emit a warning message. - protected Set<String> mUnknownTypeMap = new HashSet<String>(); - protected Set<String> mUnknownValueMap = new HashSet<String>(); - - // For measuring performance. - private long mTimeTotal; - private long mTimeReadStartRecord; - private long mTimeReadEndRecord; - private long mTimeStartProperty; - private long mTimeEndProperty; - private long mTimeParseItems; - private long mTimeParseLineAndHandleGroup; - private long mTimeParsePropertyValues; - private long mTimeParseAdrOrgN; - private long mTimeHandleMiscPropertyValue; - private long mTimeHandleQuotedPrintable; - private long mTimeHandleBase64; - - public VCardParser_V21() { - this(null); - } - - public VCardParser_V21(VCardSourceDetector detector) { - this(detector != null ? detector.getEstimatedType() : VCardConfig.PARSE_TYPE_UNKNOWN); - } - - public VCardParser_V21(int parseType) { - super(parseType); - if (parseType == VCardConfig.PARSE_TYPE_FOMA) { - mNestCount = 1; - } - } + /* package */ static final Set<String> sKnownPropertyNameSet = + Collections.unmodifiableSet(new HashSet<String>( + Arrays.asList("BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", + "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", + "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER"))); /** - * Parses the file at the given position. - * - * vcard_file = [wsls] vcard [wsls] + * A unmodifiable Set storing the types known in vCard 2.1. */ - protected void parseVCardFile() throws IOException, VCardException { - boolean firstReading = true; - while (true) { - if (mCanceled) { - break; - } - if (!parseOneVCard(firstReading)) { - break; - } - firstReading = false; - } - - if (mNestCount > 0) { - boolean useCache = true; - for (int i = 0; i < mNestCount; i++) { - readEndVCard(useCache, true); - useCache = false; - } - } - } - - protected int getVersion() { - return VCardConfig.FLAG_V21; - } - - protected String getVersionString() { - return VCardConstants.VERSION_V21; - } + /* package */ static final Set<String> sKnownTypeSet = + Collections.unmodifiableSet(new HashSet<String>( + Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", + "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS", + "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK", + "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL", + "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF", + "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", + "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", + "WAVE", "AIFF", "PCM", "X509", "PGP"))); /** - * @return true when the propertyName is a valid property name. + * A unmodifiable Set storing the values for the type "VALUE", available in the vCard 2.1. */ - protected boolean isValidPropertyName(String propertyName) { - if (!(sAvailablePropertyNameSetV21.contains(propertyName.toUpperCase()) || - propertyName.startsWith("X-")) && - !mUnknownTypeMap.contains(propertyName)) { - mUnknownTypeMap.add(propertyName); - Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); - } - return true; - } + /* package */ static final Set<String> sKnownValueSet = + Collections.unmodifiableSet(new HashSet<String>( + Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID"))); /** - * @return true when the encoding is a valid encoding. - */ - protected boolean isValidEncoding(String encoding) { - return sAvailableEncodingV21.contains(encoding.toUpperCase()); - } - - /** - * @return String. It may be null, or its length may be 0 - * @throws IOException - */ - protected String getLine() throws IOException { - return mReader.readLine(); - } - - /** - * @return String with it's length > 0 - * @throws IOException - * @throws VCardException when the stream reached end of line - */ - protected String getNonEmptyLine() throws IOException, VCardException { - String line; - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException("Reached end of buffer."); - } else if (line.trim().length() > 0) { - return line; - } - } - } - - /** - * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF - * items *CRLF - * "END" [ws] ":" [ws] "VCARD" - */ - private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException { - boolean allowGarbage = false; - if (firstReading) { - if (mNestCount > 0) { - for (int i = 0; i < mNestCount; i++) { - if (!readBeginVCard(allowGarbage)) { - return false; - } - allowGarbage = true; - } - } - } - - if (!readBeginVCard(allowGarbage)) { - return false; - } - long start; - if (mBuilder != null) { - start = System.currentTimeMillis(); - mBuilder.startEntry(); - mTimeReadStartRecord += System.currentTimeMillis() - start; - } - start = System.currentTimeMillis(); - parseItems(); - mTimeParseItems += System.currentTimeMillis() - start; - readEndVCard(true, false); - if (mBuilder != null) { - start = System.currentTimeMillis(); - mBuilder.endEntry(); - mTimeReadEndRecord += System.currentTimeMillis() - start; - } - return true; - } - - /** - * @return True when successful. False when reaching the end of line - * @throws IOException - * @throws VCardException - */ - protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { - String line; - do { - while (true) { - line = getLine(); - if (line == null) { - return false; - } else if (line.trim().length() > 0) { - break; - } - } - String[] strArray = line.split(":", 2); - int length = strArray.length; - - // Though vCard 2.1/3.0 specification does not allow lower cases, - // vCard file emitted by some external vCard expoter have such invalid Strings. - // So we allow it. - // e.g. BEGIN:vCard - if (length == 2 && - strArray[0].trim().equalsIgnoreCase("BEGIN") && - strArray[1].trim().equalsIgnoreCase("VCARD")) { - return true; - } else if (!allowGarbage) { - if (mNestCount > 0) { - mPreviousLine = line; - return false; - } else { - throw new VCardException( - "Expected String \"BEGIN:VCARD\" did not come " - + "(Instead, \"" + line + "\" came)"); - } - } - } while(allowGarbage); - - throw new VCardException("Reached where must not be reached."); - } - - /** - * The arguments useCache and allowGarbase are usually true and false accordingly when - * this function is called outside this function itself. - * - * @param useCache When true, line is obtained from mPreviousline. Otherwise, getLine() - * is used. - * @param allowGarbage When true, ignore non "END:VCARD" line. - * @throws IOException - * @throws VCardException - */ - protected void readEndVCard(boolean useCache, boolean allowGarbage) - throws IOException, VCardException { - String line; - do { - if (useCache) { - // Though vCard specification does not allow lower cases, - // some data may have them, so we allow it. - line = mPreviousLine; - } else { - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException("Expected END:VCARD was not found."); - } else if (line.trim().length() > 0) { - break; - } - } - } - - String[] strArray = line.split(":", 2); - if (strArray.length == 2 && - strArray[0].trim().equalsIgnoreCase("END") && - strArray[1].trim().equalsIgnoreCase("VCARD")) { - return; - } else if (!allowGarbage) { - throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); - } - useCache = false; - } while (allowGarbage); - } - - /** - * items = *CRLF item - * / item - */ - protected void parseItems() throws IOException, VCardException { - boolean ended = false; - - if (mBuilder != null) { - long start = System.currentTimeMillis(); - mBuilder.startProperty(); - mTimeStartProperty += System.currentTimeMillis() - start; - } - ended = parseItem(); - if (mBuilder != null && !ended) { - long start = System.currentTimeMillis(); - mBuilder.endProperty(); - mTimeEndProperty += System.currentTimeMillis() - start; - } - - while (!ended) { - // follow VCARD ,it wont reach endProperty - if (mBuilder != null) { - long start = System.currentTimeMillis(); - mBuilder.startProperty(); - mTimeStartProperty += System.currentTimeMillis() - start; - } - try { - ended = parseItem(); - } catch (VCardInvalidCommentLineException e) { - Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); - ended = false; - } - if (mBuilder != null && !ended) { - long start = System.currentTimeMillis(); - mBuilder.endProperty(); - mTimeEndProperty += System.currentTimeMillis() - start; - } - } - } - - /** - * item = [groups "."] name [params] ":" value CRLF - * / [groups "."] "ADR" [params] ":" addressparts CRLF - * / [groups "."] "ORG" [params] ":" orgparts CRLF - * / [groups "."] "N" [params] ":" nameparts CRLF - * / [groups "."] "AGENT" [params] ":" vcard CRLF - */ - protected boolean parseItem() throws IOException, VCardException { - mEncoding = sDefaultEncoding; - - final String line = getNonEmptyLine(); - long start = System.currentTimeMillis(); - - String[] propertyNameAndValue = separateLineAndHandleGroup(line); - if (propertyNameAndValue == null) { - return true; - } - if (propertyNameAndValue.length != 2) { - throw new VCardInvalidLineException("Invalid line \"" + line + "\""); - } - String propertyName = propertyNameAndValue[0].toUpperCase(); - String propertyValue = propertyNameAndValue[1]; - - mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; - - if (propertyName.equals("ADR") || propertyName.equals("ORG") || - propertyName.equals("N")) { - start = System.currentTimeMillis(); - handleMultiplePropertyValue(propertyName, propertyValue); - mTimeParseAdrOrgN += System.currentTimeMillis() - start; - return false; - } else if (propertyName.equals("AGENT")) { - handleAgent(propertyValue); - return false; - } else if (isValidPropertyName(propertyName)) { - if (propertyName.equals("BEGIN")) { - if (propertyValue.equals("VCARD")) { - throw new VCardNestedException("This vCard has nested vCard data in it."); - } else { - throw new VCardException("Unknown BEGIN type: " + propertyValue); - } - } else if (propertyName.equals("VERSION") && - !propertyValue.equals(getVersionString())) { - throw new VCardVersionException("Incompatible version: " + - propertyValue + " != " + getVersionString()); - } - start = System.currentTimeMillis(); - handlePropertyValue(propertyName, propertyValue); - mTimeParsePropertyValues += System.currentTimeMillis() - start; - return false; - } - - throw new VCardException("Unknown property name: \"" + propertyName + "\""); - } - - static private final int STATE_GROUP_OR_PROPNAME = 0; - static private final int STATE_PARAMS = 1; - // vCard 3.0 specification allows double-quoted param-value, while vCard 2.1 does not. - // This is just for safety. - static private final int STATE_PARAMS_IN_DQUOTE = 2; - - protected String[] separateLineAndHandleGroup(String line) throws VCardException { - int state = STATE_GROUP_OR_PROPNAME; - int nameIndex = 0; - - final String[] propertyNameAndValue = new String[2]; - - final int length = line.length(); - if (length > 0 && line.charAt(0) == '#') { - throw new VCardInvalidCommentLineException(); - } - - for (int i = 0; i < length; i++) { - char ch = line.charAt(i); - switch (state) { - case STATE_GROUP_OR_PROPNAME: { - if (ch == ':') { - final String propertyName = line.substring(nameIndex, i); - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; - return null; - } - if (mBuilder != null) { - mBuilder.propertyName(propertyName); - } - propertyNameAndValue[0] = propertyName; - if (i < length - 1) { - propertyNameAndValue[1] = line.substring(i + 1); - } else { - propertyNameAndValue[1] = ""; - } - return propertyNameAndValue; - } else if (ch == '.') { - String groupName = line.substring(nameIndex, i); - if (mBuilder != null) { - mBuilder.propertyGroup(groupName); - } - nameIndex = i + 1; - } else if (ch == ';') { - String propertyName = line.substring(nameIndex, i); - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; - return null; - } - if (mBuilder != null) { - mBuilder.propertyName(propertyName); - } - propertyNameAndValue[0] = propertyName; - nameIndex = i + 1; - state = STATE_PARAMS; - } - break; - } - case STATE_PARAMS: { - if (ch == '"') { - state = STATE_PARAMS_IN_DQUOTE; - } else if (ch == ';') { - handleParams(line.substring(nameIndex, i)); - nameIndex = i + 1; - } else if (ch == ':') { - handleParams(line.substring(nameIndex, i)); - if (i < length - 1) { - propertyNameAndValue[1] = line.substring(i + 1); - } else { - propertyNameAndValue[1] = ""; - } - return propertyNameAndValue; - } - break; - } - case STATE_PARAMS_IN_DQUOTE: { - if (ch == '"') { - state = STATE_PARAMS; - } - break; - } - } - } - - throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); - } - - /** - * params = ";" [ws] paramlist - * paramlist = paramlist [ws] ";" [ws] param - * / param - * param = "TYPE" [ws] "=" [ws] ptypeval - * / "VALUE" [ws] "=" [ws] pvalueval - * / "ENCODING" [ws] "=" [ws] pencodingval - * / "CHARSET" [ws] "=" [ws] charsetval - * / "LANGUAGE" [ws] "=" [ws] langval - * / "X-" word [ws] "=" [ws] word - * / knowntype - */ - protected void handleParams(String params) throws VCardException { - String[] strArray = params.split("=", 2); - if (strArray.length == 2) { - final String paramName = strArray[0].trim().toUpperCase(); - String paramValue = strArray[1].trim(); - if (paramName.equals("TYPE")) { - handleType(paramValue); - } else if (paramName.equals("VALUE")) { - handleValue(paramValue); - } else if (paramName.equals("ENCODING")) { - handleEncoding(paramValue); - } else if (paramName.equals("CHARSET")) { - handleCharset(paramValue); - } else if (paramName.equals("LANGUAGE")) { - handleLanguage(paramValue); - } else if (paramName.startsWith("X-")) { - handleAnyParam(paramName, paramValue); - } else { - throw new VCardException("Unknown type \"" + paramName + "\""); - } - } else { - handleParamWithoutName(strArray[0]); - } - } - - /** - * vCard 3.0 parser may throw VCardException. - */ - @SuppressWarnings("unused") - protected void handleParamWithoutName(final String paramValue) throws VCardException { - handleType(paramValue); - } - - /** - * ptypeval = knowntype / "X-" word - */ - protected void handleType(final String ptypeval) { - String upperTypeValue = ptypeval; - if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && - !mUnknownTypeMap.contains(ptypeval)) { - mUnknownTypeMap.add(ptypeval); - Log.w(LOG_TAG, "TYPE unsupported by vCard 2.1: " + ptypeval); - } - if (mBuilder != null) { - mBuilder.propertyParamType("TYPE"); - mBuilder.propertyParamValue(upperTypeValue); - } - } - - /** - * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word - */ - protected void handleValue(final String pvalueval) { - if (!sKnownValueSet.contains(pvalueval.toUpperCase()) && - pvalueval.startsWith("X-") && - !mUnknownValueMap.contains(pvalueval)) { - mUnknownValueMap.add(pvalueval); - Log.w(LOG_TAG, "VALUE unsupported by vCard 2.1: " + pvalueval); - } - if (mBuilder != null) { - mBuilder.propertyParamType("VALUE"); - mBuilder.propertyParamValue(pvalueval); - } - } - - /** - * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word - */ - protected void handleEncoding(String pencodingval) throws VCardException { - if (isValidEncoding(pencodingval) || - pencodingval.startsWith("X-")) { - if (mBuilder != null) { - mBuilder.propertyParamType("ENCODING"); - mBuilder.propertyParamValue(pencodingval); - } - mEncoding = pencodingval; - } else { - throw new VCardException("Unknown encoding \"" + pencodingval + "\""); - } - } - - /** - * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521), - * but today's vCard often contains other charset, so we allow them. - */ - protected void handleCharset(String charsetval) { - if (mBuilder != null) { - mBuilder.propertyParamType("CHARSET"); - mBuilder.propertyParamValue(charsetval); - } - } - - /** - * See also Section 7.1 of RFC 1521 - */ - protected void handleLanguage(String langval) throws VCardException { - String[] strArray = langval.split("-"); - if (strArray.length != 2) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - String tmp = strArray[0]; - int length = tmp.length(); - for (int i = 0; i < length; i++) { - if (!isLetter(tmp.charAt(i))) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - } - tmp = strArray[1]; - length = tmp.length(); - for (int i = 0; i < length; i++) { - if (!isLetter(tmp.charAt(i))) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - } - if (mBuilder != null) { - mBuilder.propertyParamType("LANGUAGE"); - mBuilder.propertyParamValue(langval); - } - } - - /** - * Mainly for "X-" type. This accepts any kind of type without check. - */ - protected void handleAnyParam(String paramName, String paramValue) { - if (mBuilder != null) { - mBuilder.propertyParamType(paramName); - mBuilder.propertyParamValue(paramValue); - } - } - - protected void handlePropertyValue(String propertyName, String propertyValue) - throws IOException, VCardException { - if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - final long start = System.currentTimeMillis(); - final String result = getQuotedPrintable(propertyValue); - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(result); - mBuilder.propertyValues(v); - } - mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; - } else if (mEncoding.equalsIgnoreCase("BASE64") || - mEncoding.equalsIgnoreCase("B")) { - final long start = System.currentTimeMillis(); - // It is very rare, but some BASE64 data may be so big that - // OutOfMemoryError occurs. To ignore such cases, use try-catch. - try { - final String result = getBase64(propertyValue); - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(result); - mBuilder.propertyValues(v); - } - } catch (OutOfMemoryError error) { - Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); - if (mBuilder != null) { - mBuilder.propertyValues(null); - } - } - mTimeHandleBase64 += System.currentTimeMillis() - start; - } else { - if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") - || mEncoding.equalsIgnoreCase("8BIT") - || mEncoding.toUpperCase().startsWith("X-"))) { - Log.w(LOG_TAG, "The encoding unsupported by vCard spec: \"" + mEncoding + "\"."); - } - - final long start = System.currentTimeMillis(); - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(maybeUnescapeText(propertyValue)); - mBuilder.propertyValues(v); - } - mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; - } - } - - protected String getQuotedPrintable(String firstString) throws IOException, VCardException { - // Specifically, there may be some padding between = and CRLF. - // See the following: - // - // qp-line := *(qp-segment transport-padding CRLF) - // qp-part transport-padding - // qp-segment := qp-section *(SPACE / TAB) "=" - // ; Maximum length of 76 characters - // - // e.g. (from RFC 2045) - // Now's the time = - // for all folk to come= - // to the aid of their country. - if (firstString.trim().endsWith("=")) { - // remove "transport-padding" - int pos = firstString.length() - 1; - while(firstString.charAt(pos) != '=') { - } - StringBuilder builder = new StringBuilder(); - builder.append(firstString.substring(0, pos + 1)); - builder.append("\r\n"); - String line; - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException( - "File ended during parsing quoted-printable String"); - } - if (line.trim().endsWith("=")) { - // remove "transport-padding" - pos = line.length() - 1; - while(line.charAt(pos) != '=') { - } - builder.append(line.substring(0, pos + 1)); - builder.append("\r\n"); - } else { - builder.append(line); - break; - } - } - return builder.toString(); - } else { - return firstString; - } - } - - protected String getBase64(String firstString) throws IOException, VCardException { - StringBuilder builder = new StringBuilder(); - builder.append(firstString); - - while (true) { - String line = getLine(); - if (line == null) { - throw new VCardException( - "File ended during parsing BASE64 binary"); - } - if (line.length() == 0) { - break; - } - builder.append(line); - } - - return builder.toString(); - } - - /** - * Mainly for "ADR", "ORG", and "N" - * We do not care the number of strnosemi here. - * - * addressparts = 0*6(strnosemi ";") strnosemi - * ; PO Box, Extended Addr, Street, Locality, Region, - * Postal Code, Country Name - * orgparts = *(strnosemi ";") strnosemi - * ; First is Organization Name, - * remainder are Organization Units. - * nameparts = 0*4(strnosemi ";") strnosemi - * ; Family, Given, Middle, Prefix, Suffix. - * ; Example:Public;John;Q.;Reverend Dr.;III, Esq. - * strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi - * ; To include a semicolon in this string, it must be escaped - * ; with a "\" character. - * - * We are not sure whether we should add "\" CRLF to each value. - * For now, we exclude them. + * <p> + * A unmodifiable Set storing the values for the type "ENCODING", available in the vCard 2.1. + * </p> + * <p> + * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. + * We allow it for safety. + * </p> */ - protected void handleMultiplePropertyValue(String propertyName, String propertyValue) - throws IOException, VCardException { - // vCard 2.1 does not allow QUOTED-PRINTABLE here, - // but some softwares/devices emit such data. - if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - propertyValue = getQuotedPrintable(propertyValue); - } - - if (mBuilder != null) { - mBuilder.propertyValues(VCardUtils.constructListFromValue( - propertyValue, (getVersion() == VCardConfig.FLAG_V30))); - } - } + /* package */ static final Set<String> sAvailableEncoding = + Collections.unmodifiableSet(new HashSet<String>( + Arrays.asList(VCardConstants.PARAM_ENCODING_7BIT, + VCardConstants.PARAM_ENCODING_8BIT, + VCardConstants.PARAM_ENCODING_QP, + VCardConstants.PARAM_ENCODING_BASE64, + VCardConstants.PARAM_ENCODING_B))); - /** - * vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all. - * - * item = ... - * / [groups "."] "AGENT" - * [params] ":" vcard CRLF - * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF - * items *CRLF "END" [ws] ":" [ws] "VCARD" - */ - protected void handleAgent(final String propertyValue) throws VCardException { - if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) { - // Apparently invalid line seen in Windows Mobile 6.5. Ignore them. - return; - } else { - throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); - } - // TODO: Support AGENT property. - } - - /** - * For vCard 3.0. - */ - protected String maybeUnescapeText(final String text) { - return text; - } + private final VCardParserImpl_V21 mVCardParserImpl; - /** - * Returns unescaped String if the character should be unescaped. Return null otherwise. - * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. - */ - protected String maybeUnescapeCharacter(final char ch) { - return unescapeCharacter(ch); + public VCardParser_V21() { + mVCardParserImpl = new VCardParserImpl_V21(); } - public static String unescapeCharacter(final char ch) { - // Original vCard 2.1 specification does not allow transformation - // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of - // this class allowed them, so keep it as is. - if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { - return String.valueOf(ch); - } else { - return null; - } - } - - @Override - public boolean parse(final InputStream is, final VCardInterpreter builder) - throws IOException, VCardException { - return parse(is, VCardConfig.DEFAULT_CHARSET, builder); + public VCardParser_V21(int vcardType) { + mVCardParserImpl = new VCardParserImpl_V21(vcardType); } - - @Override - public boolean parse(InputStream is, String charset, VCardInterpreter builder) - throws IOException, VCardException { - if (charset == null) { - charset = VCardConfig.DEFAULT_CHARSET; - } - final InputStreamReader tmpReader = new InputStreamReader(is, charset); - if (VCardConfig.showPerformanceLog()) { - mReader = new CustomBufferedReader(tmpReader); - } else { - mReader = new BufferedReader(tmpReader); - } - - mBuilder = builder; - long start = System.currentTimeMillis(); - if (mBuilder != null) { - mBuilder.start(); - } - parseVCardFile(); - if (mBuilder != null) { - mBuilder.end(); - } - mTimeTotal += System.currentTimeMillis() - start; - - if (VCardConfig.showPerformanceLog()) { - showPerformanceInfo(); - } - - return true; - } - - @Override - public void parse(InputStream is, String charset, VCardInterpreter builder, boolean canceled) + public void parse(InputStream is, VCardInterpreter interepreter) throws IOException, VCardException { - mCanceled = canceled; - parse(is, charset, builder); - } - - private void showPerformanceInfo() { - Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); - if (mReader instanceof CustomBufferedReader) { - Log.d(LOG_TAG, "Total readLine time: " + - ((CustomBufferedReader)mReader).getTotalmillisecond() + " ms"); - } - Log.d(LOG_TAG, "Time for handling the beggining of the record: " + - mTimeReadStartRecord + " ms"); - Log.d(LOG_TAG, "Time for handling the end of the record: " + - mTimeReadEndRecord + " ms"); - Log.d(LOG_TAG, "Time for parsing line, and handling group: " + - mTimeParseLineAndHandleGroup + " ms"); - Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); - Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); - Log.d(LOG_TAG, "Time for handling normal property values: " + - mTimeHandleMiscPropertyValue + " ms"); - Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + - mTimeHandleQuotedPrintable + " ms"); - Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); + mVCardParserImpl.parse(is, interepreter); } - private boolean isLetter(char ch) { - if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { - return true; - } - return false; - } -} - -class CustomBufferedReader extends BufferedReader { - private long mTime; - - public CustomBufferedReader(Reader in) { - super(in); - } - - @Override - public String readLine() throws IOException { - long start = System.currentTimeMillis(); - String ret = super.readLine(); - long end = System.currentTimeMillis(); - mTime += end - start; - return ret; - } - - public long getTotalmillisecond() { - return mTime; + public void cancel() { + mVCardParserImpl.cancel(); } } diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java index 4ecfe97..238d3a8 100644 --- a/core/java/android/pim/vcard/VCardParser_V30.java +++ b/core/java/android/pim/vcard/VCardParser_V30.java @@ -16,343 +16,72 @@ package android.pim.vcard; import android.pim.vcard.exception.VCardException; -import android.util.Log; import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.Set; /** - * The class used to parse vCard 3.0. - * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426). + * <p> + * vCard parser for vCard 3.0. See RFC 2426 for more detail. + * </p> + * <p> + * This parser allows vCard format which is not allowed in the RFC, since + * we have seen several vCard 3.0 files which don't comply with it. + * </p> + * <p> + * e.g. vCard 3.0 does not allow "CHARSET" attribute, but some actual files + * have it and they uses non UTF-8 charsets. UTF-8 is recommended in RFC 2426, + * but it is not a must. We silently allow "CHARSET". + * </p> */ -public class VCardParser_V30 extends VCardParser_V21 { - private static final String LOG_TAG = "VCardParser_V30"; - - private static final HashSet<String> sAcceptablePropsWithParam = new HashSet<String>( - Arrays.asList( +public class VCardParser_V30 implements VCardParser { + /* package */ static final Set<String> sKnownPropertyNameSet = + Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", // 2.1 "NAME", "PROFILE", "SOURCE", "NICKNAME", "CLASS", - "SORT-STRING", "CATEGORIES", "PRODID")); // 3.0 - - // Although "7bit" and "BASE64" is not allowed in vCard 3.0, we allow it for safety. - private static final HashSet<String> sAcceptableEncodingV30 = new HashSet<String>( - Arrays.asList("7BIT", "8BIT", "BASE64", "B")); - - // Although RFC 2426 specifies some property must not have parameters, we allow it, - // since there may be some careers which violates the RFC... - private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>(); - - private String mPreviousLine; - - private boolean mEmittedAgentWarning = false; + "SORT-STRING", "CATEGORIES", "PRODID"))); // 3.0 /** - * True when the caller wants the parser to be strict about the input. - * Currently this is only for testing. + * <p> + * A unmodifiable Set storing the values for the type "ENCODING", available in the vCard 3.0. + * </p> + * <p> + * Though vCard 2.1 specification does not allow "7BIT" or "BASE64", we allow them for safety. + * </p> + * <p> + * "QUOTED-PRINTABLE" is not allowed in vCard 3.0 and not in this parser either, + * because the encoding ambiguates how the vCard file to be parsed. + * </p> */ - private final boolean mStrictParsing; + /* package */ static final Set<String> sAcceptableEncoding = + Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( + VCardConstants.PARAM_ENCODING_7BIT, + VCardConstants.PARAM_ENCODING_8BIT, + VCardConstants.PARAM_ENCODING_BASE64, + VCardConstants.PARAM_ENCODING_B))); - public VCardParser_V30() { - super(); - mStrictParsing = false; - } - - /** - * @param strictParsing when true, this object throws VCardException when the vcard is not - * valid from the view of vCard 3.0 specification (defined in RFC 2426). Note that this class - * is not fully yet for being used with this flag and may not notice invalid line(s). - * - * @hide currently only for testing! - */ - public VCardParser_V30(boolean strictParsing) { - super(); - mStrictParsing = strictParsing; - } - - public VCardParser_V30(int parseMode) { - super(parseMode); - mStrictParsing = false; - } - - @Override - protected int getVersion() { - return VCardConfig.FLAG_V30; - } + private final VCardParserImpl_V30 mVCardParserImpl; - @Override - protected String getVersionString() { - return VCardConstants.VERSION_V30; - } - - @Override - protected boolean isValidPropertyName(String propertyName) { - if (!(sAcceptablePropsWithParam.contains(propertyName) || - acceptablePropsWithoutParam.contains(propertyName) || - propertyName.startsWith("X-")) && - !mUnknownTypeMap.contains(propertyName)) { - mUnknownTypeMap.add(propertyName); - Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName); - } - return true; - } - - @Override - protected boolean isValidEncoding(String encoding) { - return sAcceptableEncodingV30.contains(encoding.toUpperCase()); + public VCardParser_V30() { + mVCardParserImpl = new VCardParserImpl_V30(); } - @Override - protected String getLine() throws IOException { - if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } else { - return mReader.readLine(); - } - } - - /** - * vCard 3.0 requires that the line with space at the beginning of the line - * must be combined with previous line. - */ - @Override - protected String getNonEmptyLine() throws IOException, VCardException { - String line; - StringBuilder builder = null; - while (true) { - line = mReader.readLine(); - if (line == null) { - if (builder != null) { - return builder.toString(); - } else if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } - throw new VCardException("Reached end of buffer."); - } else if (line.length() == 0) { - if (builder != null) { - return builder.toString(); - } else if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } - } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { - if (builder != null) { - // See Section 5.8.1 of RFC 2425 (MIME-DIR document). - // Following is the excerpts from it. - // - // DESCRIPTION:This is a long description that exists on a long line. - // - // Can be represented as: - // - // DESCRIPTION:This is a long description - // that exists on a long line. - // - // It could also be represented as: - // - // DESCRIPTION:This is a long descrip - // tion that exists o - // n a long line. - builder.append(line.substring(1)); - } else if (mPreviousLine != null) { - builder = new StringBuilder(); - builder.append(mPreviousLine); - mPreviousLine = null; - builder.append(line.substring(1)); - } else { - throw new VCardException("Space exists at the beginning of the line"); - } - } else { - if (mPreviousLine == null) { - mPreviousLine = line; - if (builder != null) { - return builder.toString(); - } - } else { - String ret = mPreviousLine; - mPreviousLine = line; - return ret; - } - } - } - } - - - /** - * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF - * 1 * (contentline) - * ;A vCard object MUST include the VERSION, FN and N types. - * [group "."] "END" ":" "VCARD" 1 * CRLF - */ - @Override - protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { - // TODO: vCard 3.0 supports group. - return super.readBeginVCard(allowGarbage); + public VCardParser_V30(int vcardType) { + mVCardParserImpl = new VCardParserImpl_V30(vcardType); } - @Override - protected void readEndVCard(boolean useCache, boolean allowGarbage) + public void parse(InputStream is, VCardInterpreter interepreter) throws IOException, VCardException { - // TODO: vCard 3.0 supports group. - super.readEndVCard(useCache, allowGarbage); - } - - /** - * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. - */ - @Override - protected void handleParams(String params) throws VCardException { - try { - super.handleParams(params); - } catch (VCardException e) { - // maybe IANA type - String[] strArray = params.split("=", 2); - if (strArray.length == 2) { - handleAnyParam(strArray[0], strArray[1]); - } else { - // Must not come here in the current implementation. - throw new VCardException( - "Unknown params value: " + params); - } - } - } - - @Override - protected void handleAnyParam(String paramName, String paramValue) { - super.handleAnyParam(paramName, paramValue); - } - - @Override - protected void handleParamWithoutName(final String paramValue) throws VCardException { - if (mStrictParsing) { - throw new VCardException("Parameter without name is not acceptable in vCard 3.0"); - } else { - super.handleParamWithoutName(paramValue); - } - } - - /** - * vCard 3.0 defines - * - * param = param-name "=" param-value *("," param-value) - * param-name = iana-token / x-name - * param-value = ptext / quoted-string - * quoted-string = DQUOTE QSAFE-CHAR DQUOTE - */ - @Override - protected void handleType(String ptypevalues) { - String[] ptypeArray = ptypevalues.split(","); - mBuilder.propertyParamType("TYPE"); - for (String value : ptypeArray) { - int length = value.length(); - if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) { - mBuilder.propertyParamValue(value.substring(1, value.length() - 1)); - } else { - mBuilder.propertyParamValue(value); - } - } - } - - @Override - protected void handleAgent(String propertyValue) { - // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1. - // - // e.g. - // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n - // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n - // ET:jfriday@host.com\nEND:VCARD\n - // - // TODO: fix this. - // - // issue: - // vCard 3.0 also allows this as an example. - // - // AGENT;VALUE=uri: - // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com - // - // This is not vCard. Should we support this? - // - // Just ignore the line for now, since we cannot know how to handle it... - if (!mEmittedAgentWarning) { - Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); - mEmittedAgentWarning = true; - } - } - - /** - * vCard 3.0 does not require two CRLF at the last of BASE64 data. - * It only requires that data should be MIME-encoded. - */ - @Override - protected String getBase64(String firstString) throws IOException, VCardException { - StringBuilder builder = new StringBuilder(); - builder.append(firstString); - - while (true) { - String line = getLine(); - if (line == null) { - throw new VCardException( - "File ended during parsing BASE64 binary"); - } - if (line.length() == 0) { - break; - } else if (!line.startsWith(" ") && !line.startsWith("\t")) { - mPreviousLine = line; - break; - } - builder.append(line); - } - - return builder.toString(); - } - - /** - * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") - * ; \\ encodes \, \n or \N encodes newline - * ; \; encodes ;, \, encodes , - * - * Note: Apple escapes ':' into '\:' while does not escape '\' - */ - @Override - protected String maybeUnescapeText(String text) { - return unescapeText(text); - } - - public static String unescapeText(String text) { - StringBuilder builder = new StringBuilder(); - int length = text.length(); - for (int i = 0; i < length; i++) { - char ch = text.charAt(i); - if (ch == '\\' && i < length - 1) { - char next_ch = text.charAt(++i); - if (next_ch == 'n' || next_ch == 'N') { - builder.append("\n"); - } else { - builder.append(next_ch); - } - } else { - builder.append(ch); - } - } - return builder.toString(); - } - - @Override - protected String maybeUnescapeCharacter(char ch) { - return unescapeCharacter(ch); + mVCardParserImpl.parse(is, interepreter); } - public static String unescapeCharacter(char ch) { - if (ch == 'n' || ch == 'N') { - return "\n"; - } else { - return String.valueOf(ch); - } + public void cancel() { + mVCardParserImpl.cancel(); } } diff --git a/core/java/android/pim/vcard/VCardParser_V40.java b/core/java/android/pim/vcard/VCardParser_V40.java new file mode 100644 index 0000000..65a2f68 --- /dev/null +++ b/core/java/android/pim/vcard/VCardParser_V40.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 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. + */ +package android.pim.vcard; + +import android.pim.vcard.exception.VCardException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * <p> + * vCard parser for vCard 4.0. + * </p> + * <p> + * Currently this parser is based on vCard 4.0 specification rev 11. + * </p> + */ +public class VCardParser_V40 implements VCardParser { + /* package */ static final Set<String> sKnownPropertyNameSet = + Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( + "BEGIN", "END", "SOURCE", "NAME", "KIND", "XML", + "FN", "N", "NICKNAME", "PHOTO", "BDAY", "DDAY", + "BIRTH", "DEATH", "ANNIVERSARY", "SEX", "ADR", + "LABEL", "TEL", "EMAIL", "IMPP", "LANG", "TZ", + "GEO", "TITLE", "ROLE", "LOGO", "ORG", "MEMBER", + "RELATED", "CATEGORIES", "NOTE", "PRODID", + "REV", "SOUND", "UID", "CLIENTPIDMAP", + "URL", "VERSION", "CLASS", "KEY", "FBURL", "CALENDRURI", + "CALURI"))); + + /** + * <p> + * A unmodifiable Set storing the values for the type "ENCODING", available in vCard 4.0. + * </p> + */ + /* package */ static final Set<String> sAcceptableEncoding = + Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( + VCardConstants.PARAM_ENCODING_8BIT, + VCardConstants.PARAM_ENCODING_B))); + + private final VCardParserImpl_V30 mVCardParserImpl; + + public VCardParser_V40() { + mVCardParserImpl = new VCardParserImpl_V40(); + } + + public VCardParser_V40(int vcardType) { + mVCardParserImpl = new VCardParserImpl_V40(vcardType); + } + + @Override + public void parse(InputStream is, VCardInterpreter interepreter) + throws IOException, VCardException { + mVCardParserImpl.parse(is, interepreter); + } + + @Override + public void cancel() { + mVCardParserImpl.cancel(); + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardSourceDetector.java b/core/java/android/pim/vcard/VCardSourceDetector.java index 7297c50..4c6461e 100644 --- a/core/java/android/pim/vcard/VCardSourceDetector.java +++ b/core/java/android/pim/vcard/VCardSourceDetector.java @@ -15,17 +15,33 @@ */ package android.pim.vcard; +import android.text.TextUtils; +import android.util.Log; + import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; /** - * Class which tries to detects the source of the vCard from its properties. - * Currently this implementation is very premature. - * @hide + * <p> + * The class which tries to detects the source of a vCard file from its contents. + * </p> + * <p> + * The specification of vCard (including both 2.1 and 3.0) is not so strict as to + * guess its format just by reading beginning few lines (usually we can, but in + * some most pessimistic case, we cannot until at almost the end of the file). + * Also we cannot store all vCard entries in memory, while there's no specification + * how big the vCard entry would become after the parse. + * </p> + * <p> + * This class is usually used for the "first scan", in which we can understand which vCard + * version is used (and how many entries exist in a file). + * </p> */ public class VCardSourceDetector implements VCardInterpreter { + private static final String LOG_TAG = "VCardSourceDetector"; + private static Set<String> APPLE_SIGNS = new HashSet<String>(Arrays.asList( "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", "X-PHONETIC-LAST-NAME", "X-ABADR", "X-ABUID")); @@ -42,10 +58,30 @@ public class VCardSourceDetector implements VCardInterpreter { "X-SD-VERN", "X-SD-FORMAT_VER", "X-SD-CATEGORIES", "X-SD-CLASS", "X-SD-DCREATED", "X-SD-DESCRIPTION")); private static String TYPE_FOMA_CHARSET_SIGN = "X-SD-CHAR_CODE"; - - private int mType = VCardConfig.PARSE_TYPE_UNKNOWN; + + /** + * Represents that no estimation is available. Users of this class is able to this + * constant when you don't want to let a vCard parser rely on estimation for parse type. + */ + public static final int PARSE_TYPE_UNKNOWN = 0; + + // For Apple's software, which does not mean this type is effective for all its products. + // We confirmed they usually use UTF-8, but not sure about vCard type. + private static final int PARSE_TYPE_APPLE = 1; + // For Japanese mobile phones, which are usually using Shift_JIS as a charset. + private static final int PARSE_TYPE_MOBILE_PHONE_JP = 2; + // For some of mobile phones released from DoCoMo, which use nested vCard. + private static final int PARSE_TYPE_DOCOMO_TORELATE_NEST = 3; + // For Japanese Windows Mobel phones. It's version is supposed to be 6.5. + private static final int PARSE_TYPE_WINDOWS_MOBILE_V65_JP = 4; + + private int mParseType = 0; // Not sure. + + private boolean mNeedToParseVersion = false; + private int mVersion = -1; // -1 == unknown + // Some mobile phones (like FOMA) tells us the charset of the data. - private boolean mNeedParseSpecifiedCharset; + private boolean mNeedToParseCharset; private String mSpecifiedCharset; public void start() { @@ -58,7 +94,8 @@ public class VCardSourceDetector implements VCardInterpreter { } public void startProperty() { - mNeedParseSpecifiedCharset = false; + mNeedToParseCharset = false; + mNeedToParseVersion = false; } public void endProperty() { @@ -71,22 +108,26 @@ public class VCardSourceDetector implements VCardInterpreter { } public void propertyName(String name) { - if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) { - mType = VCardConfig.PARSE_TYPE_FOMA; - mNeedParseSpecifiedCharset = true; + if (name.equalsIgnoreCase(VCardConstants.PROPERTY_VERSION)) { + mNeedToParseVersion = true; + return; + } else if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) { + mParseType = PARSE_TYPE_DOCOMO_TORELATE_NEST; + // Probably Shift_JIS is used, but we should double confirm. + mNeedToParseCharset = true; return; } - if (mType != VCardConfig.PARSE_TYPE_UNKNOWN) { + if (mParseType != PARSE_TYPE_UNKNOWN) { return; } if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_WINDOWS_MOBILE_JP; + mParseType = PARSE_TYPE_WINDOWS_MOBILE_V65_JP; } else if (FOMA_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_FOMA; + mParseType = PARSE_TYPE_DOCOMO_TORELATE_NEST; } else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_MOBILE_PHONE_JP; + mParseType = PARSE_TYPE_MOBILE_PHONE_JP; } else if (APPLE_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_APPLE; + mParseType = PARSE_TYPE_APPLE; } } @@ -97,30 +138,65 @@ public class VCardSourceDetector implements VCardInterpreter { } public void propertyValues(List<String> values) { - if (mNeedParseSpecifiedCharset && values.size() > 0) { + if (mNeedToParseVersion && values.size() > 0) { + final String versionString = values.get(0); + if (versionString.equals(VCardConstants.VERSION_V21)) { + mVersion = VCardConfig.VERSION_21; + } else if (versionString.equals(VCardConstants.VERSION_V30)) { + mVersion = VCardConfig.VERSION_30; + } else if (versionString.equals(VCardConstants.VERSION_V40)) { + mVersion = VCardConfig.VERSION_40; + } else { + Log.w(LOG_TAG, "Invalid version string: " + versionString); + } + } else if (mNeedToParseCharset && values.size() > 0) { mSpecifiedCharset = values.get(0); } } - /* package */ int getEstimatedType() { - return mType; + /** + * @return The available type can be used with vCard parser. You probably need to + * use {{@link #getEstimatedCharset()} to understand the charset to be used. + */ + public int getEstimatedType() { + switch (mParseType) { + case PARSE_TYPE_DOCOMO_TORELATE_NEST: + return VCardConfig.VCARD_TYPE_DOCOMO | VCardConfig.FLAG_TORELATE_NEST; + case PARSE_TYPE_MOBILE_PHONE_JP: + return VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE; + case PARSE_TYPE_APPLE: + case PARSE_TYPE_WINDOWS_MOBILE_V65_JP: + default: { + if (mVersion == VCardConfig.VERSION_21) { + return VCardConfig.VCARD_TYPE_V21_GENERIC; + } else if (mVersion == VCardConfig.VERSION_30) { + return VCardConfig.VCARD_TYPE_V30_GENERIC; + } else if (mVersion == VCardConfig.VERSION_40) { + return VCardConfig.VCARD_TYPE_V40_GENERIC; + } else { + return VCardConfig.VCARD_TYPE_UNKNOWN; + } + } + } } - + /** - * Return charset String guessed from the source's properties. + * <p> + * Returns charset String guessed from the source's properties. * This method must be called after parsing target file(s). + * </p> * @return Charset String. Null is returned if guessing the source fails. */ public String getEstimatedCharset() { - if (mSpecifiedCharset != null) { + if (TextUtils.isEmpty(mSpecifiedCharset)) { return mSpecifiedCharset; } - switch (mType) { - case VCardConfig.PARSE_TYPE_WINDOWS_MOBILE_JP: - case VCardConfig.PARSE_TYPE_FOMA: - case VCardConfig.PARSE_TYPE_MOBILE_PHONE_JP: + switch (mParseType) { + case PARSE_TYPE_WINDOWS_MOBILE_V65_JP: + case PARSE_TYPE_DOCOMO_TORELATE_NEST: + case PARSE_TYPE_MOBILE_PHONE_JP: return "SHIFT_JIS"; - case VCardConfig.PARSE_TYPE_APPLE: + case PARSE_TYPE_APPLE: return "UTF-8"; default: return null; diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java index f972799..abceca0 100644 --- a/core/java/android/pim/vcard/VCardUtils.java +++ b/core/java/android/pim/vcard/VCardUtils.java @@ -16,13 +16,21 @@ package android.pim.vcard; import android.content.ContentProviderOperation; +import android.pim.vcard.exception.VCardException; import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.Data; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import android.util.Log; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.net.QuotedPrintableCodec; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -36,6 +44,8 @@ import java.util.Set; * Utilities for VCard handling codes. */ public class VCardUtils { + private static final String LOG_TAG = "VCardUtils"; + // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is // converted to two parameter Strings. These only contain some minor fields valid in both // vCard and current (as of 2009-08-07) Contacts structure. @@ -185,8 +195,7 @@ public class VCardUtils { // For backward compatibility. // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. // To support mobile type at that time, this custom label had been used. - return (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME.equals(label) - || sMobilePhoneLabelSet.contains(label)); + return ("_AUTO_CELL".equals(label) || sMobilePhoneLabelSet.contains(label)); } public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) { @@ -240,10 +249,13 @@ public class VCardUtils { } /** + * <p> * Inserts postal data into the builder object. - * + * </p> + * <p> * Note that the data structure of ContactsContract is different from that defined in vCard. * So some conversion may be performed in this method. + * </p> */ public static void insertStructuredPostalDataUsingContactsStruct(int vcardType, final ContentProviderOperation.Builder builder, @@ -319,18 +331,33 @@ public class VCardUtils { return builder.toString(); } + /** + * Splits the given value into pieces using the delimiter ';' inside it. + * + * Escaped characters in those values are automatically unescaped into original form. + */ public static List<String> constructListFromValue(final String value, - final boolean isV30) { + final int vcardType) { final List<String> list = new ArrayList<String>(); StringBuilder builder = new StringBuilder(); - int length = value.length(); + final int length = value.length(); for (int i = 0; i < length; i++) { char ch = value.charAt(i); if (ch == '\\' && i < length - 1) { char nextCh = value.charAt(i + 1); - final String unescapedString = - (isV30 ? VCardParser_V30.unescapeCharacter(nextCh) : - VCardParser_V21.unescapeCharacter(nextCh)); + final String unescapedString; + if (VCardConfig.isVersion40(vcardType)) { + unescapedString = VCardParserImpl_V40.unescapeCharacter(nextCh); + } else if (VCardConfig.isVersion30(vcardType)) { + unescapedString = VCardParserImpl_V30.unescapeCharacter(nextCh); + } else { + if (!VCardConfig.isVersion21(vcardType)) { + // Unknown vCard type + Log.w(LOG_TAG, "Unknown vCard type"); + } + unescapedString = VCardParserImpl_V21.unescapeCharacter(nextCh); + } + if (unescapedString != null) { builder.append(unescapedString); i++; @@ -371,9 +398,13 @@ public class VCardUtils { } /** + * <p> * This is useful when checking the string should be encoded into quoted-printable * or not, which is required by vCard 2.1. + * </p> + * <p> * See the definition of "7bit" in vCard 2.1 spec for more information. + * </p> */ public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) { if (values == null) { @@ -407,13 +438,16 @@ public class VCardUtils { new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' ')); /** + * <p> * This is useful since vCard 3.0 often requires the ("X-") properties and groups * should contain only alphabets, digits, and hyphen. - * + * </p> + * <p> * Note: It is already known some devices (wrongly) outputs properties with characters * which should not be in the field. One example is "X-GOOGLE TALK". We accept * such kind of input but must never output it unless the target is very specific - * to the device which is able to parse the malformed input. + * to the device which is able to parse the malformed input. + * </p> */ public static boolean containsOnlyAlphaDigitHyphen(final String...values) { if (values == null) { @@ -451,14 +485,39 @@ public class VCardUtils { return true; } + public static boolean containsOnlyWhiteSpaces(final String...values) { + if (values == null) { + return true; + } + return containsOnlyWhiteSpaces(Arrays.asList(values)); + } + + public static boolean containsOnlyWhiteSpaces(final Collection<String> values) { + if (values == null) { + return true; + } + for (final String str : values) { + if (TextUtils.isEmpty(str)) { + continue; + } + final int length = str.length(); + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + if (!Character.isWhitespace(str.codePointAt(i))) { + return false; + } + } + } + return true; + } + /** - * <P> + * <p> * Returns true when the given String is categorized as "word" specified in vCard spec 2.1. - * </P> - * <P> - * vCard 2.1 specifies:<BR /> + * </p> + * <p> + * vCard 2.1 specifies:<br /> * word = <any printable 7bit us-ascii except []=:., > - * </P> + * </p> */ public static boolean isV21Word(final String value) { if (TextUtils.isEmpty(value)) { @@ -477,6 +536,14 @@ public class VCardUtils { return true; } + private static final int[] sEscapeIndicatorsV30 = new int[]{ + ':', ';', ',', ' ' + }; + + private static final int[] sEscapeIndicatorsV40 = new int[]{ + ';', ':' + }; + /** * <P> * Returns String available as parameter value in vCard 3.0. @@ -487,10 +554,18 @@ public class VCardUtils { * This method checks whether the given String can be used without quotes. * </P> * <P> - * Note: We remove DQUOTE silently for now. + * Note: We remove DQUOTE inside the given value silently for now. * </P> */ - public static String toStringAvailableAsV30ParameValue(String value) { + public static String toStringAsV30ParamValue(String value) { + return toStringAsParamValue(value, sEscapeIndicatorsV30); + } + + public static String toStringAsV40ParamValue(String value) { + return toStringAsParamValue(value, sEscapeIndicatorsV40); + } + + private static String toStringAsParamValue(String value, final int[] escapeIndicators) { if (TextUtils.isEmpty(value)) { value = ""; } @@ -506,12 +581,19 @@ public class VCardUtils { continue; } builder.appendCodePoint(codePoint); - if (codePoint == ':' || codePoint == ',' || codePoint == ' ') { - needQuote = true; + for (int indicator : escapeIndicators) { + if (codePoint == indicator) { + needQuote = true; + break; + } } } + final String result = builder.toString(); - return ((needQuote || result.isEmpty()) ? ('"' + result + '"') : result); + return ((result.isEmpty() || VCardUtils.containsOnlyWhiteSpaces(result)) + ? "" + : (needQuote ? ('"' + result + '"') + : result)); } public static String toHalfWidthString(final String orgString) { @@ -577,6 +659,138 @@ public class VCardUtils { return true; } + //// The methods bellow may be used by unit test. + + /** + * Unquotes given Quoted-Printable value. value must not be null. + */ + public static String parseQuotedPrintable( + final String value, boolean strictLineBreaking, + String sourceCharset, String targetCharset) { + // "= " -> " ", "=\t" -> "\t". + // Previous code had done this replacement. Keep on the safe side. + final String quotedPrintable; + { + final StringBuilder builder = new StringBuilder(); + final int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (ch == '=' && i < length - 1) { + char nextCh = value.charAt(i + 1); + if (nextCh == ' ' || nextCh == '\t') { + builder.append(nextCh); + i++; + continue; + } + } + builder.append(ch); + } + quotedPrintable = builder.toString(); + } + + String[] lines; + if (strictLineBreaking) { + lines = quotedPrintable.split("\r\n"); + } else { + StringBuilder builder = new StringBuilder(); + final int length = quotedPrintable.length(); + ArrayList<String> list = new ArrayList<String>(); + for (int i = 0; i < length; i++) { + char ch = quotedPrintable.charAt(i); + if (ch == '\n') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else if (ch == '\r') { + list.add(builder.toString()); + builder = new StringBuilder(); + if (i < length - 1) { + char nextCh = quotedPrintable.charAt(i + 1); + if (nextCh == '\n') { + i++; + } + } + } else { + builder.append(ch); + } + } + final String lastLine = builder.toString(); + if (lastLine.length() > 0) { + list.add(lastLine); + } + lines = list.toArray(new String[0]); + } + + final StringBuilder builder = new StringBuilder(); + for (String line : lines) { + if (line.endsWith("=")) { + line = line.substring(0, line.length() - 1); + } + builder.append(line); + } + + final String rawString = builder.toString(); + if (TextUtils.isEmpty(rawString)) { + Log.w(LOG_TAG, "Given raw string is empty."); + } + + byte[] rawBytes = null; + try { + rawBytes = rawString.getBytes(sourceCharset); + } catch (UnsupportedEncodingException e) { + Log.w(LOG_TAG, "Failed to decode: " + sourceCharset); + rawBytes = rawString.getBytes(); + } + + byte[] decodedBytes = null; + try { + decodedBytes = QuotedPrintableCodec.decodeQuotedPrintable(rawBytes); + } catch (DecoderException e) { + Log.e(LOG_TAG, "DecoderException is thrown."); + decodedBytes = rawBytes; + } + + try { + return new String(decodedBytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return new String(decodedBytes); + } + } + + public static final VCardParser getAppropriateParser(int vcardType) + throws VCardException { + if (VCardConfig.isVersion21(vcardType)) { + return new VCardParser_V21(); + } else if (VCardConfig.isVersion30(vcardType)) { + return new VCardParser_V30(); + } else if (VCardConfig.isVersion40(vcardType)) { + return new VCardParser_V40(); + } else { + throw new VCardException("Version is not specified"); + } + } + + public static final String convertStringCharset( + String originalString, String sourceCharset, String targetCharset) { + if (sourceCharset.equalsIgnoreCase(targetCharset)) { + return originalString; + } + final Charset charset = Charset.forName(sourceCharset); + final ByteBuffer byteBuffer = charset.encode(originalString); + // byteBuffer.array() "may" return byte array which is larger than + // byteBuffer.remaining(). Here, we keep on the safe side. + final byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + try { + return new String(bytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return null; + } + } + + // TODO: utilities for vCard 4.0: datetime, timestamp, integer, float, and boolean + private VCardUtils() { } } diff --git a/core/java/android/pim/vcard/exception/VCardInvalidLineException.java b/core/java/android/pim/vcard/exception/VCardInvalidLineException.java index 330153e..b80584b 100644 --- a/core/java/android/pim/vcard/exception/VCardInvalidLineException.java +++ b/core/java/android/pim/vcard/exception/VCardInvalidLineException.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package android.pim.vcard.exception; /** diff --git a/core/java/android/pim/vcard/exception/VCardVersionException.java b/core/java/android/pim/vcard/exception/VCardVersionException.java index 9fe8b7f..0709fe4 100644 --- a/core/java/android/pim/vcard/exception/VCardVersionException.java +++ b/core/java/android/pim/vcard/exception/VCardVersionException.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package android.pim.vcard.exception; /** |