Merge "SipManager: always return true for SIP API and VOIP support query." into gingerbread
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 @@
         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 @@
     }
 
     /**
-     * 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.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.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 @@
     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);
+        // 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 (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;
+        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;
             }
-            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;
             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 @@
         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 @@
     }
 
     /**
+     * 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 @@
                             encodeQuotedPrintable(displayName) :
                                 escapeCharacters(displayName);
 
+            // N
             mBuilder.append(VCardConstants.PROPERTY_N);
             if (shouldAppendCharsetParam(displayName)) {
                 mBuilder.append(VCARD_PARAM_SEPARATOR);
@@ -376,11 +554,13 @@
             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 @@
             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 @@
         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 @@
             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 @@
             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 @@
                     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 @@
                 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 @@
                 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 @@
                 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 @@
                 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 @@
             // 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 @@
                     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 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 @@
         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 @@
                     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 @@
         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 @@
         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 @@
             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 @@
         // 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 @@
                 } 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 @@
         // 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 @@
         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 @@
                     break;
                 }
                 case '\\': {
-                    if (mIsV30) {
+                    if (mIsV30OrV40) {
                         tmpBuilder.append("\\\\");
                         break;
                     } else {
@@ -1911,7 +2112,7 @@
                     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..193cf1e 100644
--- a/core/java/android/pim/vcard/VCardComposer.java
+++ b/core/java/android/pim/vcard/VCardComposer.java
@@ -19,16 +19,12 @@
 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.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 @@
 
 /**
  * <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 @@
  *     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 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 @@
         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 @@
 
     /**
      * <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 @@
                             "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 @@
     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,21 +278,39 @@
     };
 
     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);
+    }
+
+    /**
+     * 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,
+    public VCardComposer(final Context context, final int vcardType, String charset,
             final boolean careHandlerErrors) {
         mContext = context;
         mVCardType = vcardType;
@@ -294,30 +318,67 @@
         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;
+        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 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;
         } 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 @@
         }
 
         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 @@
             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 @@
             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 @@
     }
 
     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 @@
         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,35 @@
             }
         }
 
-        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));
+            if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
+                builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));            
+            }
+            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();
         }
-        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 +639,38 @@
     @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..8e759e3 100644
--- a/core/java/android/pim/vcard/VCardConfig.java
+++ b/core/java/android/pim/vcard/VCardConfig.java
@@ -38,20 +38,43 @@
 
     /* 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;
+    /**
+     * <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";
 
-    // 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;
+    /**
+     * 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";
 
-    // 0x2 is reserved for the future use ...
+    /**
+     * 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 @@
     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,196 @@
      * 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. ////
 
+    public static final int FLAG_REFRAIN_IMAGE_EXPORT = 0x00800000;
+
     /**
-     * <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_UTF8_STR = "v30_generic";
-    
+    /* package */ static final String VCARD_TYPE_V30_GENERIC_STR = "v30_generic";
+
     /**
-     * <P>
+     * 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";
+
+    /**
+     * <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);
+    public static final int VCARD_TYPE_V21_JAPANESE =
+        (VERSION_21 | NAME_ORDER_JAPANESE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
 
-    /* package */ static final String VCARD_TYPE_V21_JAPANESE_UTF8_STR = "v21_japanese_utf8";
+    /* package */ static final String VCARD_TYPE_V21_JAPANESE_STR = "v21_japanese_utf8";
 
     /**
-     * <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_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 +432,20 @@
         }
     }
 
-    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 isVersion30(final int vcardType) {
+        return (vcardType & VERSION_MASK) == VERSION_30;
+    }
+
+    public static boolean isVersion40(final int vcardType) {
+        return (vcardType & VERSION_MASK) == VERSION_40;
     }
 
     public static boolean shouldUseQuotedPrintable(final int vcardType) {
-        return !isV30(vcardType);
-    }
-
-    public static boolean usesUtf8(final int vcardType) {
-        return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_UTF8);
-    }
-
-    public static boolean usesShiftJis(final int vcardType) {
-        return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_SHIFT_JIS);
+        return !isVersion30(vcardType);
     }
 
     public static int getNameOrderType(final int vcardType) {
@@ -442,7 +470,7 @@
     }
 
     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 +502,4 @@
 
     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..76371ef9 100644
--- a/core/java/android/pim/vcard/VCardConstants.java
+++ b/core/java/android/pim/vcard/VCardConstants.java
@@ -21,6 +21,7 @@
 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 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 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 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 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 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.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 @@
 
     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 @@
                 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 @@
         }
     }
 
-    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 @@
         }
     }
 
-    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 @@
         }
     }
 
-    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 @@
         }
     }
 
-    static public class ImData {
+    public static class ImData {
         public final int protocol;
         public final String customProtocol;
         public final int type;
@@ -434,6 +431,14 @@
         }
     }
 
+    // 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 @@
     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 @@
     private String mDisplayName;
 
     private String mBirthday;
+    private String mAnniversary;
 
     private List<String> mNoteList;
     private List<PhoneData> mPhoneList;
@@ -469,7 +475,7 @@
     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 @@
                 }
             }
 
-            // 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 @@
     }
 
     /**
-     * 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 @@
      * {@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 @@
         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 @@
         }
         // 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 @@
         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 @@
         }
         // 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 @@
         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 @@
         }
 
         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 @@
         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 @@
                 // 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 @@
                     }
                 }
             }
-            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 @@
             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 @@
             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 @@
      */
     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 @@
         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 @@
                 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 @@
             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 @@
                 && 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 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 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 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;
 
-    /**
-     * 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;
+    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;
+            mSourceCharset = inputCharset;
         } else {
-            mInputCharset = VCardConfig.DEFAULT_CHARSET;
+            mSourceCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET;
         }
-        if (charsetForDetodedBytes != null) {
-            mCharsetForDecodedBytes = charsetForDetodedBytes;
-        } else {
-            mCharsetForDecodedBytes = DEFAULT_CHARSET_FOR_DECODED_BYTES;
-        }
-        mStrictLineBreakParsing = strictLineBreakParsing;
+        mStrictLineBreaking = strictLineBreakParsing;
         mVCardType = vcardType;
         mAccount = account;
     }
@@ -95,60 +99,63 @@
     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 @@
             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;
+    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 "";
         }
-        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 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 @@
             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 @@
 /**
  * <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 @@
  * {@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 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)
+    public void 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
+     * <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 abstract boolean parse(InputStream is, String charset, VCardInterpreter builder)
-            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.
-     */
-    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 {
+    /**
+     * A unmodifiable Set storing the property names available in the vCard 2.1 specification.
+     */
+    /* 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")));
 
     /**
+     * A unmodifiable Set storing the types known in vCard 2.1.
+     */
+    /* 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")));
+
+    /**
+     * A unmodifiable Set storing the values for the type "VALUE", available in the vCard 2.1.
+     */
+    /* package */ static final Set<String> sKnownValueSet =
+            Collections.unmodifiableSet(new HashSet<String>(
+                    Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")));
+
+    /**
+     * <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...
+     * We allow it for safety.
+     * </p>
      */
-    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;
+    /* 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)));
 
-    /** 
-     * The encoding type. "Encoding" in vCard is different from "Charset".
-     * e.g. 7BIT, 8BIT, QUOTED-PRINTABLE. 
-     */
-    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;
+    private final VCardParserImpl_V21 mVCardParserImpl;
 
     public VCardParser_V21() {
-        this(null);
+        mVCardParserImpl = new VCardParserImpl_V21();
     }
 
-    public VCardParser_V21(VCardSourceDetector detector) {
-        this(detector != null ? detector.getEstimatedType() : VCardConfig.PARSE_TYPE_UNKNOWN);
+    public VCardParser_V21(int vcardType) {
+        mVCardParserImpl = new VCardParserImpl_V21(vcardType);
     }
 
-    public VCardParser_V21(int parseType) {
-        super(parseType);
-        if (parseType == VCardConfig.PARSE_TYPE_FOMA) {
-            mNestCount = 1;
-        }
-    }
-
-    /**
-     * Parses the file at the given position.
-     *
-     * vcard_file = [wsls] vcard [wsls]
-     */
-    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;
-    }
-
-    /**
-     * @return true when the propertyName is a valid property name.
-     */
-    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;
-    }
-
-    /**
-     * @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)
+    public void parse(InputStream is, VCardInterpreter interepreter)
             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 + "\"");
+        mVCardParserImpl.parse(is, interepreter);
     }
 
-    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.               
-     */
-    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)));
-        }
-    }
-
-    /**
-     * 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;
-    }
-
-    /**
-     * 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 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);
-    }
-    
-    @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)
-            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");
-    }
-
-    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)));
+
+    private final VCardParserImpl_V30 mVCardParserImpl;
 
     public VCardParser_V30() {
-        super();
-        mStrictParsing = false;
+        mVCardParserImpl = new VCardParserImpl_V30();
     }
 
-    /**
-     * @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 vcardType) {
+        mVCardParserImpl = new VCardParserImpl_V30(vcardType);
     }
 
-    public VCardParser_V30(int parseMode) {
-        super(parseMode);
-        mStrictParsing = false;
-    }
-
-    @Override
-    protected int getVersion() {
-        return VCardConfig.FLAG_V30;
-    }
-
-    @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());
-    }
-
-    @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)
+    public void parse(InputStream is, VCardInterpreter interepreter)
             throws IOException, VCardException {
-        // TODO: vCard 3.0 supports group.
-        super.readEndVCard(useCache, allowGarbage);
+        mVCardParserImpl.parse(is, interepreter);
     }
 
-    /**
-     * 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);
-    }
-
-    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 @@
             "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 void startProperty() {
-        mNeedParseSpecifiedCharset = false;
+        mNeedToParseCharset = false;
+        mNeedToParseVersion = false;
     }
     
     public void endProperty() {
@@ -71,22 +108,26 @@
     }
     
     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 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 charset String guessed from the source's properties.
+     * @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;
+                }
+            }
+        }
+    }
+
+    /**
+     * <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 @@
  * 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 @@
         // 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 @@
     }
 
     /**
+     * <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 @@
         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 @@
     }
 
     /**
+     * <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 @@
         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 @@
         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 = &lt;any printable 7bit us-ascii except []=:., &gt;
-     * </P>
+     * </p>
      */
     public static boolean isV21Word(final String value) {
         if (TextUtils.isEmpty(value)) {
@@ -477,6 +536,14 @@
         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 @@
      * 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 @@
                 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 @@
         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;
 
 /**
diff --git a/core/tests/coretests/res/raw/v21_im.vcf b/core/tests/coretests/res/raw/v21_im.vcf
new file mode 100644
index 0000000..cc1aabb
--- /dev/null
+++ b/core/tests/coretests/res/raw/v21_im.vcf
@@ -0,0 +1,5 @@
+BEGIN:VCARD
+VERSION:2.1
+X-ANDROID-CUSTOM:vnd.android.cursor.item/nickname;Nick;1;;;;;;;;;;;;;
+X-GOOGLE-TALK:hhh@gmail.com
+END:VCARD
diff --git a/core/tests/coretests/res/raw/v21_invalid_multiple_line.vcf b/core/tests/coretests/res/raw/v21_invalid_multiple_line.vcf
new file mode 100644
index 0000000..9c81fd5
--- /dev/null
+++ b/core/tests/coretests/res/raw/v21_invalid_multiple_line.vcf
@@ -0,0 +1,7 @@
+BEGIN:VCARD
+VERSION:2.1
+N:;Omega;;;
+EMAIL;INTERNET:"Omega"
+  <omega@example.com>
+FN:Omega
+END:VCARD
diff --git a/core/tests/coretests/res/raw/v30_comma_separated.vcf b/core/tests/coretests/res/raw/v30_comma_separated.vcf
index 98a7f20..f1baf88 100644
--- a/core/tests/coretests/res/raw/v30_comma_separated.vcf
+++ b/core/tests/coretests/res/raw/v30_comma_separated.vcf
@@ -1,5 +1,5 @@
 BEGIN:VCARD

 VERSION:3.0

-N:F;G;M;;

-TEL;TYPE=PAGER,WORK,MSG:6101231234@pagersample.com

+N;TYPE=PREF,HOME:F;G;M;;

+TEL;TYPE="COMMA,SEPARATED:INSIDE.DQUOTE",PREF:1

 END:VCARD

diff --git a/core/tests/coretests/res/raw/v30_pager.vcf b/core/tests/coretests/res/raw/v30_pager.vcf
new file mode 100644
index 0000000..98a7f20
--- /dev/null
+++ b/core/tests/coretests/res/raw/v30_pager.vcf
@@ -0,0 +1,5 @@
+BEGIN:VCARD

+VERSION:3.0

+N:F;G;M;;

+TEL;TYPE=PAGER,WORK,MSG:6101231234@pagersample.com

+END:VCARD

diff --git a/core/tests/coretests/res/raw/v40_sort_as.vcf b/core/tests/coretests/res/raw/v40_sort_as.vcf
new file mode 100644
index 0000000..6f6bc3b
--- /dev/null
+++ b/core/tests/coretests/res/raw/v40_sort_as.vcf
@@ -0,0 +1,6 @@
+BEGIN:VCARD

+VERSION:4.0

+FN:安藤 ロイド

+N;SORT-AS="あんどう;ろいど":安藤;ロイド;;;

+ORG;TYPE=WORK;SORT-AS="ぐーぐる;けんさくぶもん":グーグル;検索部門

+END:VCARD

diff --git a/core/tests/coretests/src/android/bluetooth/BluetoothRebootStressTest.java b/core/tests/coretests/src/android/bluetooth/BluetoothRebootStressTest.java
new file mode 100644
index 0000000..33e9dd7
--- /dev/null
+++ b/core/tests/coretests/src/android/bluetooth/BluetoothRebootStressTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.bluetooth;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+
+/**
+ * Instrumentation test case for stress test involving rebooting the device.
+ * <p>
+ * This test case tests that bluetooth is enabled after a device reboot. Because
+ * the device will reboot, the instrumentation must be driven by a script on the
+ * host side.
+ */
+public class BluetoothRebootStressTest extends InstrumentationTestCase {
+    private static final String TAG = "BluetoothRebootStressTest";
+    private static final String OUTPUT_FILE = "BluetoothRebootStressTestOutput.txt";
+
+    private BluetoothTestUtils mTestUtils;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        Context context = getInstrumentation().getTargetContext();
+        mTestUtils = new BluetoothTestUtils(context, TAG, OUTPUT_FILE);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+
+        mTestUtils.close();
+    }
+
+    /**
+     * Test method used to start the test by turning bluetooth on.
+     */
+    public void testStart() {
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        mTestUtils.enable(adapter);
+    }
+
+    /**
+     * Test method used in the middle iterations of the test to check if
+     * bluetooth is on. Does not toggle bluetooth after the check. Assumes that
+     * bluetooth has been turned on by {@code #testStart()}
+     */
+    public void testMiddleNoToggle() {
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+        assertTrue(adapter.isEnabled());
+    }
+
+    /**
+     * Test method used in the middle iterations of the test to check if
+     * bluetooth is on. Toggles bluetooth after the check. Assumes that
+     * bluetooth has been turned on by {@code #testStart()}
+     */
+    public void testMiddleToggle() {
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+        assertTrue(adapter.isEnabled());
+
+        mTestUtils.disable(adapter);
+        mTestUtils.enable(adapter);
+    }
+
+    /**
+     * Test method used in the stop the test by turning bluetooth off. Assumes
+     * that bluetooth has been turned on by {@code #testStart()}
+     */
+    public void testStop() {
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+        assertTrue(adapter.isEnabled());
+
+        mTestUtils.disable(adapter);
+    }
+}
diff --git a/core/tests/coretests/src/android/bluetooth/BluetoothStressTest.java b/core/tests/coretests/src/android/bluetooth/BluetoothStressTest.java
index ca6ece0..149685c 100644
--- a/core/tests/coretests/src/android/bluetooth/BluetoothStressTest.java
+++ b/core/tests/coretests/src/android/bluetooth/BluetoothStressTest.java
@@ -16,329 +16,28 @@
 
 package android.bluetooth;
 
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.util.Set;
-
-import android.app.Instrumentation;
-import android.bluetooth.BluetoothHeadset.ServiceListener;
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Environment;
 import android.test.InstrumentationTestCase;
-import android.util.Log;
 
 public class BluetoothStressTest extends InstrumentationTestCase {
     private static final String TAG = "BluetoothStressTest";
     private static final String OUTPUT_FILE = "BluetoothStressTestOutput.txt";
 
-    /**
-     * Timeout for {@link BluetoothAdapter#disable()} in ms.
-     */
-    private static final int DISABLE_TIMEOUT = 5000;
-
-    /**
-     * Timeout for {@link BluetoothAdapter#enable()} in ms.
-     */
-    private static final int ENABLE_TIMEOUT = 20000;
-
-    /**
-     * Timeout for {@link BluetoothAdapter#setScanMode(int)} in ms.
-     */
-    private static final int SET_SCAN_MODE_TIMEOUT = 5000;
-
-    /**
-     * Timeout for {@link BluetoothAdapter#startDiscovery()} in ms.
-     */
-    private static final int START_DISCOVERY_TIMEOUT = 5000;
-
-    /**
-     * Timeout for {@link BluetoothAdapter#cancelDiscovery()} in ms.
-     */
-    private static final int CANCEL_DISCOVERY_TIMEOUT = 5000;
-
-    /**
-     * Timeout for {@link BluetoothDevice#createBond()} in ms.
-     */
-    private static final int PAIR_TIMEOUT = 20000;
-
-    /**
-     * Timeout for {@link BluetoothDevice#removeBond()} in ms.
-     */
-    private static final int UNPAIR_TIMEOUT = 20000;
-
-    /**
-     * Timeout for {@link BluetoothA2dp#connectSink(BluetoothDevice)} in ms.
-     */
-    private static final int CONNECT_A2DP_TIMEOUT = 20000;
-
-    /**
-     * Timeout for {@link BluetoothA2dp#disconnectSink(BluetoothDevice)} in ms.
-     */
-    private static final int DISCONNECT_A2DP_TIMEOUT = 20000;
-
-    /**
-     * Timeout for {@link BluetoothHeadset#connectHeadset(BluetoothDevice)} in ms.
-     */
-    private static final int CONNECT_HEADSET_TIMEOUT = 20000;
-
-    /**
-     * Timeout for {@link BluetoothHeadset#disconnectHeadset(BluetoothDevice)} in ms.
-     */
-    private static final int DISCONNECT_HEADSET_TIMEOUT = 20000;
-
-    private static final int DISCOVERY_STARTED_FLAG = 1;
-    private static final int DISCOVERY_FINISHED_FLAG = 1 << 1;
-    private static final int SCAN_MODE_NONE_FLAG = 1 << 2;
-    private static final int SCAN_MODE_CONNECTABLE_FLAG = 1 << 3;
-    private static final int SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG = 1 << 4;
-    private static final int STATE_OFF_FLAG = 1 << 5;
-    private static final int STATE_TURNING_ON_FLAG = 1 << 6;
-    private static final int STATE_ON_FLAG = 1 << 7;
-    private static final int STATE_TURNING_OFF_FLAG = 1 << 8;
-    private static final int PAIR_STATE_FLAG = 1 << 9;
-    private static final int PROFILE_A2DP_FLAG = 1 << 10;
-    private static final int PROFILE_HEADSET_FLAG = 1 << 11;
-
-    private static final int PAIR_STATE_BONDED = 1;
-    private static final int PAIR_STATE_BONDING = 1 << 1;
-    private static final int PAIR_STATE_NONE = 1 << 2;
-
-    private static final int A2DP_STATE_DISCONNECTED = 1;
-    private static final int A2DP_STATE_CONNECTING = 1 << 1;
-    private static final int A2DP_STATE_CONNECTED = 1 << 2;
-    private static final int A2DP_STATE_DISCONNECTING = 1 << 3;
-    private static final int A2DP_STATE_PLAYING = 1 << 4;
-
-    private static final int HEADSET_STATE_DISCONNECTED = 1;
-    private static final int HEADSET_STATE_CONNECTING = 1 << 1;
-    private static final int HEADSET_STATE_CONNECTED = 1 << 2;
-
-    /**
-     * Time between polls in ms.
-     */
-    private static final int POLL_TIME = 100;
-
-    private Context mContext;
-
-    private Instrumentation mInstrumentation;
-
-    private BufferedWriter mOutputWriter;
-
-    private BluetoothA2dp mA2dp;
-
-    private BluetoothHeadset mHeadset;
-
-    private class HeadsetServiceListener implements ServiceListener {
-        private boolean mConnected = false;
-
-        @Override
-        public void onServiceConnected() {
-            synchronized (this) {
-                mConnected = true;
-            }
-        }
-
-        @Override
-        public void onServiceDisconnected() {
-            synchronized (this) {
-                mConnected = false;
-            }
-        }
-
-        public boolean isConnected() {
-            synchronized (this) {
-                return mConnected;
-            }
-        }
-    }
-
-    private HeadsetServiceListener mHeadsetServiceListener = new HeadsetServiceListener();
-
-    private class BluetoothReceiver extends BroadcastReceiver {
-        private int mFiredFlags = 0;
-        private int mPairFiredFlags = 0;
-        private int mA2dpFiredFlags = 0;
-        private int mHeadsetFiredFlags = 0;
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            synchronized (this) {
-                if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(intent.getAction())) {
-                    mFiredFlags |= DISCOVERY_STARTED_FLAG;
-                } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(intent.getAction())) {
-                    mFiredFlags |= DISCOVERY_FINISHED_FLAG;
-                } else if (BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(intent.getAction())) {
-                    int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
-                            BluetoothAdapter.ERROR);
-                    assertNotSame(mode, BluetoothAdapter.ERROR);
-                    switch (mode) {
-                        case BluetoothAdapter.SCAN_MODE_NONE:
-                            mFiredFlags |= SCAN_MODE_NONE_FLAG;
-                            break;
-                        case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
-                            mFiredFlags |= SCAN_MODE_CONNECTABLE_FLAG;
-                            break;
-                        case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
-                            mFiredFlags |= SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG;
-                            break;
-                    }
-                } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
-                    int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
-                            BluetoothAdapter.ERROR);
-                    assertNotSame(state, BluetoothAdapter.ERROR);
-                    switch (state) {
-                        case BluetoothAdapter.STATE_OFF:
-                            mFiredFlags |= STATE_OFF_FLAG;
-                            break;
-                        case BluetoothAdapter.STATE_TURNING_ON:
-                            mFiredFlags |= STATE_TURNING_ON_FLAG;
-                            break;
-                        case BluetoothAdapter.STATE_ON:
-                            mFiredFlags |= STATE_ON_FLAG;
-                            break;
-                        case BluetoothAdapter.STATE_TURNING_OFF:
-                            mFiredFlags |= STATE_TURNING_OFF_FLAG;
-                            break;
-                    }
-                } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
-                    mFiredFlags |= PAIR_STATE_FLAG;
-                    int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
-                    assertNotSame(state, -1);
-                    switch (state) {
-                        case BluetoothDevice.BOND_BONDED:
-                            mPairFiredFlags |= PAIR_STATE_BONDED;
-                            break;
-                        case BluetoothDevice.BOND_BONDING:
-                            mPairFiredFlags |= PAIR_STATE_BONDING;
-                            break;
-                        case BluetoothDevice.BOND_NONE:
-                            mPairFiredFlags |= PAIR_STATE_NONE;
-                            break;
-                    }
-                } else if (BluetoothA2dp.ACTION_SINK_STATE_CHANGED.equals(intent.getAction())) {
-                    mFiredFlags |= PROFILE_A2DP_FLAG;
-                    int state = intent.getIntExtra(BluetoothA2dp.EXTRA_SINK_STATE, -1);
-                    assertNotSame(state, -1);
-                    switch (state) {
-                        case BluetoothA2dp.STATE_DISCONNECTED:
-                            mA2dpFiredFlags |= A2DP_STATE_DISCONNECTED;
-                            break;
-                        case BluetoothA2dp.STATE_CONNECTING:
-                            mA2dpFiredFlags |= A2DP_STATE_CONNECTING;
-                            break;
-                        case BluetoothA2dp.STATE_CONNECTED:
-                            mA2dpFiredFlags |= A2DP_STATE_CONNECTED;
-                            break;
-                        case BluetoothA2dp.STATE_DISCONNECTING:
-                            mA2dpFiredFlags |= A2DP_STATE_DISCONNECTING;
-                            break;
-                        case BluetoothA2dp.STATE_PLAYING:
-                            mA2dpFiredFlags |= A2DP_STATE_PLAYING;
-                            break;
-                    }
-                } else if (BluetoothHeadset.ACTION_STATE_CHANGED.equals(intent.getAction())) {
-                    mFiredFlags |= PROFILE_HEADSET_FLAG;
-                    int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
-                            BluetoothHeadset.STATE_ERROR);
-                    assertNotSame(state, BluetoothHeadset.STATE_ERROR);
-                    switch (state) {
-                        case BluetoothHeadset.STATE_DISCONNECTED:
-                            mHeadsetFiredFlags |= HEADSET_STATE_DISCONNECTED;
-                            break;
-                        case BluetoothHeadset.STATE_CONNECTING:
-                            mHeadsetFiredFlags |= HEADSET_STATE_CONNECTING;
-                            break;
-                        case BluetoothHeadset.STATE_CONNECTED:
-                            mHeadsetFiredFlags |= HEADSET_STATE_CONNECTED;
-                            break;
-                    }
-                }
-            }
-        }
-
-        public int getFiredFlags() {
-            synchronized (this) {
-                return mFiredFlags;
-            }
-        }
-
-        public int getPairFiredFlags() {
-            synchronized (this) {
-                return mPairFiredFlags;
-            }
-        }
-
-        public int getA2dpFiredFlags() {
-            synchronized (this) {
-                return mA2dpFiredFlags;
-            }
-        }
-
-        public int getHeadsetFiredFlags() {
-            synchronized (this) {
-                return mHeadsetFiredFlags;
-            }
-        }
-
-        public void resetFiredFlags() {
-            synchronized (this) {
-                mFiredFlags = 0;
-                mPairFiredFlags = 0;
-                mA2dpFiredFlags = 0;
-                mHeadsetFiredFlags = 0;
-            }
-        }
-    }
-
-    private BluetoothReceiver mReceiver = new BluetoothReceiver();
+    private BluetoothTestUtils mTestUtils;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
 
-        mInstrumentation = getInstrumentation();
-        mContext = mInstrumentation.getTargetContext();
-
-        try {
-            mOutputWriter = new BufferedWriter(new FileWriter(new File(
-                    Environment.getExternalStorageDirectory(), OUTPUT_FILE), true));
-        } catch (IOException e) {
-            Log.w(TAG, "Test output file could not be opened", e);
-            mOutputWriter = null;
-        }
-
-        IntentFilter filter = new IntentFilter();
-        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
-        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
-        filter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
-        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
-        filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
-        filter.addAction(BluetoothA2dp.ACTION_SINK_STATE_CHANGED);
-        filter.addAction(BluetoothHeadset.ACTION_STATE_CHANGED);
-        mContext.registerReceiver(mReceiver, filter);
-
-        mA2dp = new BluetoothA2dp(mContext);
-        mHeadset = new BluetoothHeadset(mContext, mHeadsetServiceListener);
+        Context context = getInstrumentation().getTargetContext();
+        mTestUtils = new BluetoothTestUtils(context, TAG, OUTPUT_FILE);
     }
 
     @Override
     protected void tearDown() throws Exception {
         super.tearDown();
 
-        mContext.unregisterReceiver(mReceiver);
-
-        if (mOutputWriter != null) {
-            try {
-                mOutputWriter.close();
-            } catch (IOException e) {
-                Log.w(TAG, "Test output file could not be closed", e);
-            }
-        }
+        mTestUtils.close();
     }
 
     public void testEnable() {
@@ -346,48 +45,48 @@
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
 
         for (int i = 0; i < iterations; i++) {
-            writeOutput("enable iteration " + (i + 1) + " of " + iterations);
-            enable(adapter);
-            disable(adapter);
+            mTestUtils.writeOutput("enable iteration " + (i + 1) + " of " + iterations);
+            mTestUtils.enable(adapter);
+            mTestUtils.disable(adapter);
         }
     }
 
     public void testDiscoverable() {
         int iterations = BluetoothTestRunner.sDiscoverableIterations;
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-        enable(adapter);
+        mTestUtils.enable(adapter);
 
         for (int i = 0; i < iterations; i++) {
-            writeOutput("discoverable iteration " + (i + 1) + " of " + iterations);
-            discoverable(adapter);
-            undiscoverable(adapter);
+            mTestUtils.writeOutput("discoverable iteration " + (i + 1) + " of " + iterations);
+            mTestUtils.discoverable(adapter);
+            mTestUtils.undiscoverable(adapter);
         }
 
-        disable(adapter);
+        mTestUtils.disable(adapter);
     }
 
     public void testScan() {
         int iterations = BluetoothTestRunner.sScanIterations;
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-        enable(adapter);
+        mTestUtils.enable(adapter);
 
         for (int i = 0; i < iterations; i++) {
-            writeOutput("scan iteration " + (i + 1) + " of " + iterations);
-            startScan(adapter);
-            stopScan(adapter);
+            mTestUtils.writeOutput("scan iteration " + (i + 1) + " of " + iterations);
+            mTestUtils.startScan(adapter);
+            mTestUtils.stopScan(adapter);
         }
 
-        disable(adapter);
+        mTestUtils.disable(adapter);
     }
 
     public void testPair() {
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
         BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sHeadsetAddress);
 
-        enable(adapter);
-        pair(adapter, device);
-        unpair(adapter, device);
-        disable(adapter);
+        mTestUtils.enable(adapter);
+        mTestUtils.pair(adapter, device);
+        mTestUtils.unpair(adapter, device);
+        mTestUtils.disable(adapter);
     }
 
     public void testConnectA2dp() {
@@ -395,17 +94,17 @@
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
         BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sA2dpAddress);
 
-        enable(adapter);
-        pair(adapter, device);
+        mTestUtils.enable(adapter);
+        mTestUtils.pair(adapter, device);
 
         for (int i = 0; i < iterations; i++) {
-            writeOutput("connectA2dp iteration " + (i + 1) + " of " + iterations);
-            connectA2dp(adapter, device);
-            disconnectA2dp(adapter, device);
+            mTestUtils.writeOutput("connectA2dp iteration " + (i + 1) + " of " + iterations);
+            mTestUtils.connectA2dp(adapter, device);
+            mTestUtils.disconnectA2dp(adapter, device);
         }
 
         // TODO: Unpair from device if device can accept pairing after unpairing
-        disable(adapter);
+        mTestUtils.disable(adapter);
     }
 
     public void testConnectHeadset() {
@@ -413,618 +112,16 @@
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
         BluetoothDevice device = adapter.getRemoteDevice(BluetoothTestRunner.sHeadsetAddress);
 
-        enable(adapter);
-        pair(adapter, device);
+        mTestUtils.enable(adapter);
+        mTestUtils.pair(adapter, device);
 
         for (int i = 0; i < iterations; i++) {
-            writeOutput("connectHeadset iteration " + (i + 1) + " of " + iterations);
-            connectHeadset(adapter, device);
-            disconnectHeadset(adapter, device);
+            mTestUtils.writeOutput("connectHeadset iteration " + (i + 1) + " of " + iterations);
+            mTestUtils.connectHeadset(adapter, device);
+            mTestUtils.disconnectHeadset(adapter, device);
         }
 
-        disable(adapter);
-    }
-
-    private void disable(BluetoothAdapter adapter) {
-        int mask = STATE_TURNING_OFF_FLAG | STATE_OFF_FLAG | SCAN_MODE_NONE_FLAG;
-        mReceiver.resetFiredFlags();
-
-        int state = adapter.getState();
-        switch (state) {
-            case BluetoothAdapter.STATE_OFF:
-                assertFalse(adapter.isEnabled());
-                return;
-            case BluetoothAdapter.STATE_ON:
-                assertTrue(adapter.isEnabled());
-                assertTrue(adapter.disable());
-                break;
-            case BluetoothAdapter.STATE_TURNING_ON:
-                assertFalse(adapter.isEnabled());
-                assertTrue(adapter.disable());
-                break;
-            case BluetoothAdapter.STATE_TURNING_OFF:
-                assertFalse(adapter.isEnabled());
-                mask = 0; // Don't check for received intents since we might have missed them.
-                break;
-            default:
-                fail("disable() invalid state: state=" + state);
-        }
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < DISABLE_TIMEOUT) {
-            state = adapter.getState();
-            if (state == BluetoothAdapter.STATE_OFF) {
-                assertFalse(adapter.isEnabled());
-                if ((mReceiver.getFiredFlags() & mask) == mask) {
-                    mReceiver.resetFiredFlags();
-                    writeOutput(String.format("disable() completed in %d ms",
-                            (System.currentTimeMillis() - s)));
-                    return;
-                }
-            } else {
-                assertFalse(adapter.isEnabled());
-                assertEquals(BluetoothAdapter.STATE_TURNING_OFF, state);
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("disable() timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
-                state, BluetoothAdapter.STATE_OFF, firedFlags, mask));
-    }
-
-    private void enable(BluetoothAdapter adapter) {
-        int mask = STATE_TURNING_ON_FLAG | STATE_ON_FLAG | SCAN_MODE_CONNECTABLE_FLAG;
-        mReceiver.resetFiredFlags();
-
-        int state = adapter.getState();
-        switch (state) {
-            case BluetoothAdapter.STATE_ON:
-                assertTrue(adapter.isEnabled());
-                return;
-            case BluetoothAdapter.STATE_OFF:
-            case BluetoothAdapter.STATE_TURNING_OFF:
-                assertFalse(adapter.isEnabled());
-                assertTrue(adapter.enable());
-                break;
-            case BluetoothAdapter.STATE_TURNING_ON:
-                assertFalse(adapter.isEnabled());
-                mask = 0; // Don't check for received intents since we might have missed them.
-                break;
-            default:
-                fail("enable() invalid state: state=" + state);
-        }
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < ENABLE_TIMEOUT) {
-            state = adapter.getState();
-            if (state == BluetoothAdapter.STATE_ON) {
-                assertTrue(adapter.isEnabled());
-                if ((mReceiver.getFiredFlags() & mask) == mask) {
-                    mReceiver.resetFiredFlags();
-                    writeOutput(String.format("enable() completed in %d ms",
-                            (System.currentTimeMillis() - s)));
-                    return;
-                }
-            } else {
-                assertFalse(adapter.isEnabled());
-                assertEquals(BluetoothAdapter.STATE_TURNING_ON, state);
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("enable() timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
-                state, BluetoothAdapter.STATE_ON, firedFlags, mask));
-    }
-
-    private void discoverable(BluetoothAdapter adapter) {
-        int mask = SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG;
-        mReceiver.resetFiredFlags();
-
-        if (!adapter.isEnabled()) {
-            fail("discoverable() bluetooth not enabled");
-        }
-
-        int scanMode = adapter.getScanMode();
-        if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
-            return;
-        }
-
-        assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE);
-        assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE));
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < SET_SCAN_MODE_TIMEOUT) {
-            scanMode = adapter.getScanMode();
-            if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
-                if ((mReceiver.getFiredFlags() & mask) == mask) {
-                    mReceiver.resetFiredFlags();
-                    writeOutput(String.format("discoverable() completed in %d ms",
-                            (System.currentTimeMillis() - s)));
-                    return;
-                }
-            } else {
-                assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE);
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("discoverable() timeout: scanMode=%d (expected %d), flags=0x%x "
-                + "(expected 0x%x)", scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE,
-                firedFlags, mask));
-    }
-
-    private void undiscoverable(BluetoothAdapter adapter) {
-        int mask = SCAN_MODE_CONNECTABLE_FLAG;
-        mReceiver.resetFiredFlags();
-
-        if (!adapter.isEnabled()) {
-            fail("undiscoverable() bluetooth not enabled");
-        }
-
-        int scanMode = adapter.getScanMode();
-        if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
-            return;
-        }
-
-        assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
-        assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE));
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < SET_SCAN_MODE_TIMEOUT) {
-            scanMode = adapter.getScanMode();
-            if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
-                if ((mReceiver.getFiredFlags() & mask) == mask) {
-                    mReceiver.resetFiredFlags();
-                    writeOutput(String.format("undiscoverable() completed in %d ms",
-                            (System.currentTimeMillis() - s)));
-                    return;
-                }
-            } else {
-                assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("undiscoverable() timeout: scanMode=%d (expected %d), flags=0x%x "
-                + "(expected 0x%x)", scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE, firedFlags,
-                mask));
-    }
-
-    private void startScan(BluetoothAdapter adapter) {
-        int mask = DISCOVERY_STARTED_FLAG;
-        mReceiver.resetFiredFlags();
-
-        if (!adapter.isEnabled()) {
-            fail("startScan() bluetooth not enabled");
-        }
-
-        if (adapter.isDiscovering()) {
-            return;
-        }
-
-        assertTrue(adapter.startDiscovery());
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < START_DISCOVERY_TIMEOUT) {
-            if (adapter.isDiscovering() && ((mReceiver.getFiredFlags() & mask) == mask)) {
-                mReceiver.resetFiredFlags();
-                writeOutput(String.format("startScan() completed in %d ms",
-                        (System.currentTimeMillis() - s)));
-                return;
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("startScan() timeout: isDiscovering=%b, flags=0x%x (expected 0x%x)",
-                adapter.isDiscovering(), firedFlags, mask));
-    }
-
-    private void stopScan(BluetoothAdapter adapter) {
-        int mask = DISCOVERY_FINISHED_FLAG;
-        mReceiver.resetFiredFlags();
-
-        if (!adapter.isEnabled()) {
-            fail("stopScan() bluetooth not enabled");
-        }
-
-        if (!adapter.isDiscovering()) {
-            return;
-        }
-
-        // TODO: put assertTrue() around cancelDiscovery() once it starts
-        // returning true.
-        adapter.cancelDiscovery();
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < CANCEL_DISCOVERY_TIMEOUT) {
-            if (!adapter.isDiscovering() && ((mReceiver.getFiredFlags() & mask) == mask)) {
-                mReceiver.resetFiredFlags();
-                writeOutput(String.format("stopScan() completed in %d ms",
-                        (System.currentTimeMillis() - s)));
-                return;
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("stopScan() timeout: isDiscovering=%b, flags=0x%x (expected 0x%x)",
-                adapter.isDiscovering(), firedFlags, mask));
-
-    }
-
-    private void pair(BluetoothAdapter adapter, BluetoothDevice device) {
-        int mask = PAIR_STATE_FLAG;
-        int pairMask = PAIR_STATE_BONDING | PAIR_STATE_BONDED;
-        mReceiver.resetFiredFlags();
-
-        if (!adapter.isEnabled()) {
-            fail("pair() bluetooth not enabled");
-        }
-
-        int state = device.getBondState();
-        switch (state) {
-            case BluetoothDevice.BOND_BONDED:
-                assertTrue(adapter.getBondedDevices().contains(device));
-                return;
-            case BluetoothDevice.BOND_BONDING:
-                // Don't check for received intents since we might have missed them.
-                mask = pairMask = 0;
-                break;
-            case BluetoothDevice.BOND_NONE:
-                assertFalse(adapter.getBondedDevices().contains(device));
-                assertTrue(device.createBond());
-                break;
-            default:
-                fail("pair() invalide state: state=" + state);
-        }
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < PAIR_TIMEOUT) {
-            state = device.getBondState();
-            if (state == BluetoothDevice.BOND_BONDED) {
-                assertTrue(adapter.getBondedDevices().contains(device));
-                if ((mReceiver.getFiredFlags() & mask) == mask
-                        && (mReceiver.getPairFiredFlags() & pairMask) == pairMask) {
-                    writeOutput(String.format("pair() completed in %d ms: device=%s",
-                            (System.currentTimeMillis() - s), device));
-                    return;
-                }
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        int pairFiredFlags = mReceiver.getPairFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("pair() timeout: state=%d (expected %d), flags=0x%x (expected 0x%x), "
-                + "pairFlags=0x%x (expected 0x%x)", state, BluetoothDevice.BOND_BONDED, firedFlags,
-                mask, pairFiredFlags, pairMask));
-    }
-
-    private void unpair(BluetoothAdapter adapter, BluetoothDevice device) {
-        int mask = PAIR_STATE_FLAG;
-        int pairMask = PAIR_STATE_NONE;
-        mReceiver.resetFiredFlags();
-
-        if (!adapter.isEnabled()) {
-            fail("unpair() bluetooth not enabled");
-        }
-
-        int state = device.getBondState();
-        switch (state) {
-            case BluetoothDevice.BOND_BONDED:
-                assertTrue(adapter.getBondedDevices().contains(device));
-                assertTrue(device.removeBond());
-                break;
-            case BluetoothDevice.BOND_BONDING:
-                assertTrue(device.removeBond());
-                break;
-            case BluetoothDevice.BOND_NONE:
-                assertFalse(adapter.getBondedDevices().contains(device));
-                return;
-            default:
-                fail("unpair() invalid state: state=" + state);
-        }
-
-        assertTrue(device.removeBond());
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < UNPAIR_TIMEOUT) {
-            if (device.getBondState() == BluetoothDevice.BOND_NONE) {
-                assertFalse(adapter.getBondedDevices().contains(device));
-                if ((mReceiver.getFiredFlags() & mask) == mask
-                        && (mReceiver.getPairFiredFlags() & pairMask) == pairMask) {
-                    writeOutput(String.format("unpair() completed in %d ms: device=%s",
-                            (System.currentTimeMillis() - s), device));
-                    return;
-                }
-            }
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        int pairFiredFlags = mReceiver.getPairFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("unpair() timeout: state=%d (expected %d), flags=0x%x (expected 0x%x), "
-                + "pairFlags=0x%x (expected 0x%x)", state, BluetoothDevice.BOND_BONDED, firedFlags,
-                mask, pairFiredFlags, pairMask));
-    }
-
-    private void connectA2dp(BluetoothAdapter adapter, BluetoothDevice device) {
-        int mask = PROFILE_A2DP_FLAG;
-        int a2dpMask1 = A2DP_STATE_CONNECTING | A2DP_STATE_CONNECTED | A2DP_STATE_PLAYING;
-        int a2dpMask2 = a2dpMask1 ^ A2DP_STATE_CONNECTED;
-        int a2dpMask3 = a2dpMask1 ^ A2DP_STATE_PLAYING;
-        mReceiver.resetFiredFlags();
-
-        if (!adapter.isEnabled()) {
-            fail("connectA2dp() bluetooth not enabled");
-        }
-
-        if (!adapter.getBondedDevices().contains(device)) {
-            fail("connectA2dp() device not paired: device=" + device);
-        }
-
-        int state = mA2dp.getSinkState(device);
-        switch (state) {
-            case BluetoothA2dp.STATE_CONNECTED:
-            case BluetoothA2dp.STATE_PLAYING:
-                assertTrue(mA2dp.isSinkConnected(device));
-                return;
-            case BluetoothA2dp.STATE_DISCONNECTING:
-            case BluetoothA2dp.STATE_DISCONNECTED:
-                assertFalse(mA2dp.isSinkConnected(device));
-                assertTrue(mA2dp.connectSink(device));
-                break;
-            case BluetoothA2dp.STATE_CONNECTING:
-                assertFalse(mA2dp.isSinkConnected(device));
-                // Don't check for received intents since we might have missed them.
-                mask = a2dpMask1 = a2dpMask2 = a2dpMask3 = 0;
-                break;
-            default:
-                fail("connectA2dp() invalid state: state=" + state);
-        }
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < CONNECT_A2DP_TIMEOUT) {
-            state = mA2dp.getSinkState(device);
-            if (state == BluetoothA2dp.STATE_CONNECTED || state == BluetoothA2dp.STATE_PLAYING) {
-                assertTrue(mA2dp.isSinkConnected(device));
-                // Check whether STATE_CONNECTING and (STATE_CONNECTED or STATE_PLAYING) intents
-                // have fired if we are checking if intents should be fired.
-                int firedFlags = mReceiver.getFiredFlags();
-                int a2dpFiredFlags = mReceiver.getA2dpFiredFlags();
-                if ((mReceiver.getFiredFlags() & mask) == mask
-                        && ((a2dpFiredFlags & a2dpMask1) == a2dpMask1
-                                || (a2dpFiredFlags & a2dpMask2) == a2dpMask2
-                                || (a2dpFiredFlags & a2dpMask3) == a2dpMask3)) {
-                    mReceiver.resetFiredFlags();
-                    writeOutput(String.format("connectA2dp() completed in %d ms: device=%s",
-                            (System.currentTimeMillis() - s), device));
-                    return;
-                }
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        int a2dpFiredFlags = mReceiver.getA2dpFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("connectA2dp() timeout: state=%d (expected %d or %d), "
-                + "flags=0x%x (expected 0x%x), a2dpFlags=0x%x (expected 0x%x or 0x%x or 0x%x)",
-                state, BluetoothHeadset.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING, firedFlags,
-                mask, a2dpFiredFlags, a2dpMask1, a2dpMask2, a2dpMask3));
-    }
-
-    private void disconnectA2dp(BluetoothAdapter adapter, BluetoothDevice device) {
-        int mask = PROFILE_A2DP_FLAG;
-        int a2dpMask = A2DP_STATE_DISCONNECTING | A2DP_STATE_DISCONNECTED;
-        mReceiver.resetFiredFlags();
-
-        if (!adapter.isEnabled()) {
-            fail("disconnectA2dp() bluetooth not enabled");
-        }
-
-        if (!adapter.getBondedDevices().contains(device)) {
-            fail("disconnectA2dp() device not paired: device=" + device);
-        }
-
-        int state = mA2dp.getSinkState(device);
-        switch (state) {
-            case BluetoothA2dp.STATE_DISCONNECTED:
-                assertFalse(mA2dp.isSinkConnected(device));
-                return;
-            case BluetoothA2dp.STATE_CONNECTED:
-            case BluetoothA2dp.STATE_PLAYING:
-                assertTrue(mA2dp.isSinkConnected(device));
-                assertTrue(mA2dp.disconnectSink(device));
-                break;
-            case BluetoothA2dp.STATE_CONNECTING:
-                assertFalse(mA2dp.isSinkConnected(device));
-                assertTrue(mA2dp.disconnectSink(device));
-                break;
-            case BluetoothA2dp.STATE_DISCONNECTING:
-                assertFalse(mA2dp.isSinkConnected(device));
-                // Don't check for received intents since we might have missed them.
-                mask = a2dpMask = 0;
-                break;
-            default:
-                fail("disconnectA2dp() invalid state: state=" + state);
-        }
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < DISCONNECT_A2DP_TIMEOUT) {
-            state = mA2dp.getSinkState(device);
-            if (state == BluetoothA2dp.STATE_DISCONNECTED) {
-                assertFalse(mA2dp.isSinkConnected(device));
-                if ((mReceiver.getFiredFlags() & mask) == mask
-                        && (mReceiver.getA2dpFiredFlags() & a2dpMask) == a2dpMask) {
-                    mReceiver.resetFiredFlags();
-                    writeOutput(String.format("disconnectA2dp() completed in %d ms: device=%s",
-                            (System.currentTimeMillis() - s), device));
-                    return;
-                }
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        int a2dpFiredFlags = mReceiver.getA2dpFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("disconnectA2dp() timeout: state=%d (expected %d), "
-                + "flags=0x%x (expected 0x%x), a2dpFlags=0x%x (expected 0x%x)", state,
-                BluetoothA2dp.STATE_DISCONNECTED, firedFlags, mask, a2dpFiredFlags, a2dpMask));
-    }
-
-    private void connectHeadset(BluetoothAdapter adapter, BluetoothDevice device) {
-        int mask = PROFILE_HEADSET_FLAG;
-        int headsetMask = HEADSET_STATE_CONNECTING | HEADSET_STATE_CONNECTED;
-        mReceiver.resetFiredFlags();
-
-        if (!adapter.isEnabled()) {
-            fail("connectHeadset() bluetooth not enabled");
-        }
-
-        if (!adapter.getBondedDevices().contains(device)) {
-            fail("connectHeadset() device not paired: device=" + device);
-        }
-
-        while (!mHeadsetServiceListener.isConnected()) {
-            sleep(POLL_TIME);
-        }
-
-        int state = mHeadset.getState(device);
-        switch (state) {
-            case BluetoothHeadset.STATE_CONNECTED:
-                assertTrue(mHeadset.isConnected(device));
-                return;
-            case BluetoothHeadset.STATE_DISCONNECTED:
-                assertFalse(mHeadset.isConnected(device));
-                mHeadset.connectHeadset(device);
-                break;
-            case BluetoothHeadset.STATE_CONNECTING:
-                assertFalse(mHeadset.isConnected(device));
-                // Don't check for received intents since we might have missed them.
-                mask = headsetMask = 0;
-                break;
-            case BluetoothHeadset.STATE_ERROR:
-                fail("connectHeadset() error state");
-                break;
-            default:
-                fail("connectHeadset() invalid state: state=" + state);
-        }
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < CONNECT_HEADSET_TIMEOUT) {
-            state = mHeadset.getState(device);
-            if (state == BluetoothHeadset.STATE_CONNECTED) {
-                assertTrue(mHeadset.isConnected(device));
-                if ((mReceiver.getFiredFlags() & mask) == mask
-                        && (mReceiver.getHeadsetFiredFlags() & headsetMask) == headsetMask) {
-                    mReceiver.resetFiredFlags();
-                    writeOutput(String.format("connectHeadset() completed in %d ms: device=%s",
-                            (System.currentTimeMillis() - s), device));
-                    return;
-                }
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        int headsetFiredFlags = mReceiver.getHeadsetFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("connectHeadset() timeout: state=%d (expected %d), "
-                + "flags=0x%x (expected 0x%x), headsetFlags=0x%s (expected 0x%x)", state,
-                BluetoothHeadset.STATE_CONNECTED, firedFlags, mask, headsetFiredFlags,
-                headsetMask));
-    }
-
-    private void disconnectHeadset(BluetoothAdapter adapter, BluetoothDevice device) {
-        int mask = PROFILE_HEADSET_FLAG;
-        int headsetMask = HEADSET_STATE_DISCONNECTED;
-        mReceiver.resetFiredFlags();
-
-        if (!adapter.isEnabled()) {
-            fail("disconnectHeadset() bluetooth not enabled");
-        }
-
-        if (!adapter.getBondedDevices().contains(device)) {
-            fail("disconnectHeadset() device not paired: device=" + device);
-        }
-
-        while (!mHeadsetServiceListener.isConnected()) {
-            sleep(POLL_TIME);
-        }
-
-        int state = mHeadset.getState(device);
-        switch (state) {
-            case BluetoothHeadset.STATE_CONNECTED:
-                mHeadset.disconnectHeadset(device);
-                break;
-            case BluetoothHeadset.STATE_CONNECTING:
-                mHeadset.disconnectHeadset(device);
-                break;
-            case BluetoothHeadset.STATE_DISCONNECTED:
-                return;
-            case BluetoothHeadset.STATE_ERROR:
-                fail("disconnectHeadset() error state");
-                break;
-            default:
-                fail("disconnectHeadset() invalid state: state=" + state);
-        }
-
-        long s = System.currentTimeMillis();
-        while (System.currentTimeMillis() - s < DISCONNECT_HEADSET_TIMEOUT) {
-            state = mHeadset.getState(device);
-            if (state == BluetoothHeadset.STATE_DISCONNECTED) {
-                assertFalse(mHeadset.isConnected(device));
-                if ((mReceiver.getFiredFlags() & mask) == mask
-                        && (mReceiver.getHeadsetFiredFlags() & headsetMask) == headsetMask) {
-                    mReceiver.resetFiredFlags();
-                    writeOutput(String.format("disconnectHeadset() completed in %d ms: device=%s",
-                            (System.currentTimeMillis() - s), device));
-                    return;
-                }
-            }
-            sleep(POLL_TIME);
-        }
-
-        int firedFlags = mReceiver.getFiredFlags();
-        int headsetFiredFlags = mReceiver.getHeadsetFiredFlags();
-        mReceiver.resetFiredFlags();
-        fail(String.format("disconnectHeadset() timeout: state=%d (expected %d), "
-                + "flags=0x%x (expected 0x%x), headsetFlags=0x%s (expected 0x%x)", state,
-                BluetoothHeadset.STATE_DISCONNECTED, firedFlags, mask, headsetFiredFlags,
-                headsetMask));
-    }
-
-    private void writeOutput(String s) {
-        if (mOutputWriter == null) {
-            return;
-        }
-        try {
-            Log.i(TAG, s);
-            mOutputWriter.write(s + "\n");
-            mOutputWriter.flush();
-        } catch (IOException e) {
-            Log.w(TAG, "Could not write to output file", e);
-        }
-    }
-
-    private void sleep(long time) {
-        try {
-            Thread.sleep(time);
-        } catch (InterruptedException e) {
-        }
+        // TODO: Unpair from device if device can accept pairing after unpairing
+        mTestUtils.disable(adapter);
     }
 }
diff --git a/core/tests/coretests/src/android/bluetooth/BluetoothTestRunner.java b/core/tests/coretests/src/android/bluetooth/BluetoothTestRunner.java
index e3d2eb6..2e6daa3 100644
--- a/core/tests/coretests/src/android/bluetooth/BluetoothTestRunner.java
+++ b/core/tests/coretests/src/android/bluetooth/BluetoothTestRunner.java
@@ -26,8 +26,8 @@
     public static int sEnableIterations = 100;
     public static int sDiscoverableIterations = 1000;
     public static int sScanIterations = 1000;
-    public static int sConnectHeadsetIterations = 1000;
-    public static int sConnectA2dpIterations = 1000;
+    public static int sConnectHeadsetIterations = 100;
+    public static int sConnectA2dpIterations = 100;
 
     public static String sHeadsetAddress = "";
     public static String sA2dpAddress = "";
diff --git a/core/tests/coretests/src/android/bluetooth/BluetoothTestUtils.java b/core/tests/coretests/src/android/bluetooth/BluetoothTestUtils.java
new file mode 100644
index 0000000..e9311e0
--- /dev/null
+++ b/core/tests/coretests/src/android/bluetooth/BluetoothTestUtils.java
@@ -0,0 +1,939 @@
+/*
+ * 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.bluetooth;
+
+import android.bluetooth.BluetoothHeadset.ServiceListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Environment;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+public class BluetoothTestUtils extends Assert {
+
+    /**
+     * Timeout for {@link BluetoothAdapter#disable()} in ms.
+     */
+    private static final int DISABLE_TIMEOUT = 5000;
+
+    /**
+     * Timeout for {@link BluetoothAdapter#enable()} in ms.
+     */
+    private static final int ENABLE_TIMEOUT = 20000;
+
+    /**
+     * Timeout for {@link BluetoothAdapter#setScanMode(int)} in ms.
+     */
+    private static final int SET_SCAN_MODE_TIMEOUT = 5000;
+
+    /**
+     * Timeout for {@link BluetoothAdapter#startDiscovery()} in ms.
+     */
+    private static final int START_DISCOVERY_TIMEOUT = 5000;
+
+    /**
+     * Timeout for {@link BluetoothAdapter#cancelDiscovery()} in ms.
+     */
+    private static final int CANCEL_DISCOVERY_TIMEOUT = 5000;
+
+    /**
+     * Timeout for {@link BluetoothDevice#createBond()} in ms.
+     */
+    private static final int PAIR_TIMEOUT = 20000;
+
+    /**
+     * Timeout for {@link BluetoothDevice#removeBond()} in ms.
+     */
+    private static final int UNPAIR_TIMEOUT = 20000;
+
+    /**
+     * Timeout for {@link BluetoothA2dp#connectSink(BluetoothDevice)} in ms.
+     */
+    private static final int CONNECT_A2DP_TIMEOUT = 20000;
+
+    /**
+     * Timeout for {@link BluetoothA2dp#disconnectSink(BluetoothDevice)} in ms.
+     */
+    private static final int DISCONNECT_A2DP_TIMEOUT = 20000;
+
+    /**
+     * Timeout for {@link BluetoothHeadset#connectHeadset(BluetoothDevice)} in ms.
+     */
+    private static final int CONNECT_HEADSET_TIMEOUT = 20000;
+
+    /**
+     * Timeout for {@link BluetoothHeadset#disconnectHeadset(BluetoothDevice)} in ms.
+     */
+    private static final int DISCONNECT_HEADSET_TIMEOUT = 20000;
+
+    private static final int DISCOVERY_STARTED_FLAG = 1;
+    private static final int DISCOVERY_FINISHED_FLAG = 1 << 1;
+    private static final int SCAN_MODE_NONE_FLAG = 1 << 2;
+    private static final int SCAN_MODE_CONNECTABLE_FLAG = 1 << 3;
+    private static final int SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG = 1 << 4;
+    private static final int STATE_OFF_FLAG = 1 << 5;
+    private static final int STATE_TURNING_ON_FLAG = 1 << 6;
+    private static final int STATE_ON_FLAG = 1 << 7;
+    private static final int STATE_TURNING_OFF_FLAG = 1 << 8;
+    private static final int PAIR_STATE_FLAG = 1 << 9;
+    private static final int PROFILE_A2DP_FLAG = 1 << 10;
+    private static final int PROFILE_HEADSET_FLAG = 1 << 11;
+
+    private static final int PAIR_STATE_BONDED = 1;
+    private static final int PAIR_STATE_BONDING = 1 << 1;
+    private static final int PAIR_STATE_NONE = 1 << 2;
+
+    private static final int A2DP_STATE_DISCONNECTED = 1;
+    private static final int A2DP_STATE_CONNECTING = 1 << 1;
+    private static final int A2DP_STATE_CONNECTED = 1 << 2;
+    private static final int A2DP_STATE_DISCONNECTING = 1 << 3;
+    private static final int A2DP_STATE_PLAYING = 1 << 4;
+
+    private static final int HEADSET_STATE_DISCONNECTED = 1;
+    private static final int HEADSET_STATE_CONNECTING = 1 << 1;
+    private static final int HEADSET_STATE_CONNECTED = 1 << 2;
+
+    /**
+     * Time between polls in ms.
+     */
+    private static final int POLL_TIME = 100;
+
+    private Context mContext;
+
+    private BufferedWriter mOutputWriter;
+
+    private BluetoothA2dp mA2dp;
+
+    private BluetoothHeadset mHeadset;
+
+    private String mOutputFile;
+    private String mTag;
+    private class HeadsetServiceListener implements ServiceListener {
+        private boolean mConnected = false;
+
+        @Override
+        public void onServiceConnected() {
+            synchronized (this) {
+                mConnected = true;
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected() {
+            synchronized (this) {
+                mConnected = false;
+            }
+        }
+
+        public boolean isConnected() {
+            synchronized (this) {
+                return mConnected;
+            }
+        }
+    }
+
+    private HeadsetServiceListener mHeadsetServiceListener = new HeadsetServiceListener();
+
+    private class BluetoothReceiver extends BroadcastReceiver {
+        private int mFiredFlags = 0;
+        private int mPairFiredFlags = 0;
+        private int mA2dpFiredFlags = 0;
+        private int mHeadsetFiredFlags = 0;
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            synchronized (this) {
+                if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(intent.getAction())) {
+                    mFiredFlags |= DISCOVERY_STARTED_FLAG;
+                } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(intent.getAction())) {
+                    mFiredFlags |= DISCOVERY_FINISHED_FLAG;
+                } else if (BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(intent.getAction())) {
+                    int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
+                            BluetoothAdapter.ERROR);
+                    assertNotSame(mode, BluetoothAdapter.ERROR);
+                    switch (mode) {
+                        case BluetoothAdapter.SCAN_MODE_NONE:
+                            mFiredFlags |= SCAN_MODE_NONE_FLAG;
+                            break;
+                        case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
+                            mFiredFlags |= SCAN_MODE_CONNECTABLE_FLAG;
+                            break;
+                        case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
+                            mFiredFlags |= SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG;
+                            break;
+                    }
+                } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
+                    int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+                            BluetoothAdapter.ERROR);
+                    assertNotSame(state, BluetoothAdapter.ERROR);
+                    switch (state) {
+                        case BluetoothAdapter.STATE_OFF:
+                            mFiredFlags |= STATE_OFF_FLAG;
+                            break;
+                        case BluetoothAdapter.STATE_TURNING_ON:
+                            mFiredFlags |= STATE_TURNING_ON_FLAG;
+                            break;
+                        case BluetoothAdapter.STATE_ON:
+                            mFiredFlags |= STATE_ON_FLAG;
+                            break;
+                        case BluetoothAdapter.STATE_TURNING_OFF:
+                            mFiredFlags |= STATE_TURNING_OFF_FLAG;
+                            break;
+                    }
+                } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
+                    mFiredFlags |= PAIR_STATE_FLAG;
+                    int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
+                    assertNotSame(state, -1);
+                    switch (state) {
+                        case BluetoothDevice.BOND_BONDED:
+                            mPairFiredFlags |= PAIR_STATE_BONDED;
+                            break;
+                        case BluetoothDevice.BOND_BONDING:
+                            mPairFiredFlags |= PAIR_STATE_BONDING;
+                            break;
+                        case BluetoothDevice.BOND_NONE:
+                            mPairFiredFlags |= PAIR_STATE_NONE;
+                            break;
+                    }
+                } else if (BluetoothA2dp.ACTION_SINK_STATE_CHANGED.equals(intent.getAction())) {
+                    mFiredFlags |= PROFILE_A2DP_FLAG;
+                    int state = intent.getIntExtra(BluetoothA2dp.EXTRA_SINK_STATE, -1);
+                    assertNotSame(state, -1);
+                    switch (state) {
+                        case BluetoothA2dp.STATE_DISCONNECTED:
+                            mA2dpFiredFlags |= A2DP_STATE_DISCONNECTED;
+                            break;
+                        case BluetoothA2dp.STATE_CONNECTING:
+                            mA2dpFiredFlags |= A2DP_STATE_CONNECTING;
+                            break;
+                        case BluetoothA2dp.STATE_CONNECTED:
+                            mA2dpFiredFlags |= A2DP_STATE_CONNECTED;
+                            break;
+                        case BluetoothA2dp.STATE_DISCONNECTING:
+                            mA2dpFiredFlags |= A2DP_STATE_DISCONNECTING;
+                            break;
+                        case BluetoothA2dp.STATE_PLAYING:
+                            mA2dpFiredFlags |= A2DP_STATE_PLAYING;
+                            break;
+                    }
+                } else if (BluetoothHeadset.ACTION_STATE_CHANGED.equals(intent.getAction())) {
+                    mFiredFlags |= PROFILE_HEADSET_FLAG;
+                    int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+                            BluetoothHeadset.STATE_ERROR);
+                    assertNotSame(state, BluetoothHeadset.STATE_ERROR);
+                    switch (state) {
+                        case BluetoothHeadset.STATE_DISCONNECTED:
+                            mHeadsetFiredFlags |= HEADSET_STATE_DISCONNECTED;
+                            break;
+                        case BluetoothHeadset.STATE_CONNECTING:
+                            mHeadsetFiredFlags |= HEADSET_STATE_CONNECTING;
+                            break;
+                        case BluetoothHeadset.STATE_CONNECTED:
+                            mHeadsetFiredFlags |= HEADSET_STATE_CONNECTED;
+                            break;
+                    }
+                }
+            }
+        }
+
+        public int getFiredFlags() {
+            synchronized (this) {
+                return mFiredFlags;
+            }
+        }
+
+        public int getPairFiredFlags() {
+            synchronized (this) {
+                return mPairFiredFlags;
+            }
+        }
+
+        public int getA2dpFiredFlags() {
+            synchronized (this) {
+                return mA2dpFiredFlags;
+            }
+        }
+
+        public int getHeadsetFiredFlags() {
+            synchronized (this) {
+                return mHeadsetFiredFlags;
+            }
+        }
+
+        public void resetFiredFlags() {
+            synchronized (this) {
+                mFiredFlags = 0;
+                mPairFiredFlags = 0;
+                mA2dpFiredFlags = 0;
+                mHeadsetFiredFlags = 0;
+            }
+        }
+    }
+
+    private BluetoothReceiver mReceiver = new BluetoothReceiver();
+
+    public BluetoothTestUtils(Context context, String tag) {
+        this(context, tag, null);
+    }
+
+    public BluetoothTestUtils(Context context, String tag, String outputFile) {
+        mContext = context;
+        mTag = tag;
+        mOutputFile = outputFile;
+
+        if (mOutputFile == null) {
+            mOutputWriter = null;
+        } else {
+            try {
+                mOutputWriter = new BufferedWriter(new FileWriter(new File(
+                        Environment.getExternalStorageDirectory(), mOutputFile), true));
+            } catch (IOException e) {
+                Log.w(mTag, "Test output file could not be opened", e);
+                mOutputWriter = null;
+            }
+        }
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
+        filter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
+        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+        mContext.registerReceiver(mReceiver, filter);
+    }
+
+    public void close() {
+        mContext.unregisterReceiver(mReceiver);
+
+        if (mOutputWriter != null) {
+            try {
+                mOutputWriter.close();
+            } catch (IOException e) {
+                Log.w(mTag, "Test output file could not be closed", e);
+            }
+        }
+    }
+
+    public void enable(BluetoothAdapter adapter) {
+        int mask = STATE_TURNING_ON_FLAG | STATE_ON_FLAG | SCAN_MODE_CONNECTABLE_FLAG;
+        mReceiver.resetFiredFlags();
+
+        int state = adapter.getState();
+        switch (state) {
+            case BluetoothAdapter.STATE_ON:
+                assertTrue(adapter.isEnabled());
+                return;
+            case BluetoothAdapter.STATE_OFF:
+            case BluetoothAdapter.STATE_TURNING_OFF:
+                assertFalse(adapter.isEnabled());
+                assertTrue(adapter.enable());
+                break;
+            case BluetoothAdapter.STATE_TURNING_ON:
+                assertFalse(adapter.isEnabled());
+                mask = 0; // Don't check for received intents since we might have missed them.
+                break;
+            default:
+                fail("enable() invalid state: state=" + state);
+        }
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < ENABLE_TIMEOUT) {
+            state = adapter.getState();
+            if (state == BluetoothAdapter.STATE_ON) {
+                assertTrue(adapter.isEnabled());
+                if ((mReceiver.getFiredFlags() & mask) == mask) {
+                    mReceiver.resetFiredFlags();
+                    writeOutput(String.format("enable() completed in %d ms",
+                            (System.currentTimeMillis() - s)));
+                    return;
+                }
+            } else {
+                assertFalse(adapter.isEnabled());
+                assertEquals(BluetoothAdapter.STATE_TURNING_ON, state);
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("enable() timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
+                state, BluetoothAdapter.STATE_ON, firedFlags, mask));
+    }
+
+    public void disable(BluetoothAdapter adapter) {
+        int mask = STATE_TURNING_OFF_FLAG | STATE_OFF_FLAG | SCAN_MODE_NONE_FLAG;
+        mReceiver.resetFiredFlags();
+
+        int state = adapter.getState();
+        switch (state) {
+            case BluetoothAdapter.STATE_OFF:
+                assertFalse(adapter.isEnabled());
+                return;
+            case BluetoothAdapter.STATE_ON:
+                assertTrue(adapter.isEnabled());
+                assertTrue(adapter.disable());
+                break;
+            case BluetoothAdapter.STATE_TURNING_ON:
+                assertFalse(adapter.isEnabled());
+                assertTrue(adapter.disable());
+                break;
+            case BluetoothAdapter.STATE_TURNING_OFF:
+                assertFalse(adapter.isEnabled());
+                mask = 0; // Don't check for received intents since we might have missed them.
+                break;
+            default:
+                fail("disable() invalid state: state=" + state);
+        }
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < DISABLE_TIMEOUT) {
+            state = adapter.getState();
+            if (state == BluetoothAdapter.STATE_OFF) {
+                assertFalse(adapter.isEnabled());
+                if ((mReceiver.getFiredFlags() & mask) == mask) {
+                    mReceiver.resetFiredFlags();
+                    writeOutput(String.format("disable() completed in %d ms",
+                            (System.currentTimeMillis() - s)));
+                    return;
+                }
+            } else {
+                assertFalse(adapter.isEnabled());
+                assertEquals(BluetoothAdapter.STATE_TURNING_OFF, state);
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("disable() timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
+                state, BluetoothAdapter.STATE_OFF, firedFlags, mask));
+    }
+
+    public void discoverable(BluetoothAdapter adapter) {
+        int mask = SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("discoverable() bluetooth not enabled");
+        }
+
+        int scanMode = adapter.getScanMode();
+        if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+            return;
+        }
+
+        assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+        assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE));
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < SET_SCAN_MODE_TIMEOUT) {
+            scanMode = adapter.getScanMode();
+            if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+                if ((mReceiver.getFiredFlags() & mask) == mask) {
+                    mReceiver.resetFiredFlags();
+                    writeOutput(String.format("discoverable() completed in %d ms",
+                            (System.currentTimeMillis() - s)));
+                    return;
+                }
+            } else {
+                assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("discoverable() timeout: scanMode=%d (expected %d), flags=0x%x "
+                + "(expected 0x%x)", scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE,
+                firedFlags, mask));
+    }
+
+    public void undiscoverable(BluetoothAdapter adapter) {
+        int mask = SCAN_MODE_CONNECTABLE_FLAG;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("undiscoverable() bluetooth not enabled");
+        }
+
+        int scanMode = adapter.getScanMode();
+        if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
+            return;
+        }
+
+        assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+        assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE));
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < SET_SCAN_MODE_TIMEOUT) {
+            scanMode = adapter.getScanMode();
+            if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
+                if ((mReceiver.getFiredFlags() & mask) == mask) {
+                    mReceiver.resetFiredFlags();
+                    writeOutput(String.format("undiscoverable() completed in %d ms",
+                            (System.currentTimeMillis() - s)));
+                    return;
+                }
+            } else {
+                assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("undiscoverable() timeout: scanMode=%d (expected %d), flags=0x%x "
+                + "(expected 0x%x)", scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE, firedFlags,
+                mask));
+    }
+
+    public void startScan(BluetoothAdapter adapter) {
+        int mask = DISCOVERY_STARTED_FLAG;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("startScan() bluetooth not enabled");
+        }
+
+        if (adapter.isDiscovering()) {
+            return;
+        }
+
+        assertTrue(adapter.startDiscovery());
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < START_DISCOVERY_TIMEOUT) {
+            if (adapter.isDiscovering() && ((mReceiver.getFiredFlags() & mask) == mask)) {
+                mReceiver.resetFiredFlags();
+                writeOutput(String.format("startScan() completed in %d ms",
+                        (System.currentTimeMillis() - s)));
+                return;
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("startScan() timeout: isDiscovering=%b, flags=0x%x (expected 0x%x)",
+                adapter.isDiscovering(), firedFlags, mask));
+    }
+
+    public void stopScan(BluetoothAdapter adapter) {
+        int mask = DISCOVERY_FINISHED_FLAG;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("stopScan() bluetooth not enabled");
+        }
+
+        if (!adapter.isDiscovering()) {
+            return;
+        }
+
+        // TODO: put assertTrue() around cancelDiscovery() once it starts returning true.
+        adapter.cancelDiscovery();
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < CANCEL_DISCOVERY_TIMEOUT) {
+            if (!adapter.isDiscovering() && ((mReceiver.getFiredFlags() & mask) == mask)) {
+                mReceiver.resetFiredFlags();
+                writeOutput(String.format("stopScan() completed in %d ms",
+                        (System.currentTimeMillis() - s)));
+                return;
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("stopScan() timeout: isDiscovering=%b, flags=0x%x (expected 0x%x)",
+                adapter.isDiscovering(), firedFlags, mask));
+
+    }
+
+    public void pair(BluetoothAdapter adapter, BluetoothDevice device) {
+        int mask = PAIR_STATE_FLAG;
+        int pairMask = PAIR_STATE_BONDING | PAIR_STATE_BONDED;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("pair() bluetooth not enabled");
+        }
+
+        int state = device.getBondState();
+        switch (state) {
+            case BluetoothDevice.BOND_BONDED:
+                assertTrue(adapter.getBondedDevices().contains(device));
+                return;
+            case BluetoothDevice.BOND_BONDING:
+                // Don't check for received intents since we might have missed them.
+                mask = pairMask = 0;
+                break;
+            case BluetoothDevice.BOND_NONE:
+                assertFalse(adapter.getBondedDevices().contains(device));
+                assertTrue(device.createBond());
+                break;
+            default:
+                fail("pair() invalide state: state=" + state);
+        }
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < PAIR_TIMEOUT) {
+            state = device.getBondState();
+            if (state == BluetoothDevice.BOND_BONDED) {
+                assertTrue(adapter.getBondedDevices().contains(device));
+                if ((mReceiver.getFiredFlags() & mask) == mask
+                        && (mReceiver.getPairFiredFlags() & pairMask) == pairMask) {
+                    writeOutput(String.format("pair() completed in %d ms: device=%s",
+                            (System.currentTimeMillis() - s), device));
+                    return;
+                }
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        int pairFiredFlags = mReceiver.getPairFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("pair() timeout: state=%d (expected %d), flags=0x%x (expected 0x%x), "
+                + "pairFlags=0x%x (expected 0x%x)", state, BluetoothDevice.BOND_BONDED, firedFlags,
+                mask, pairFiredFlags, pairMask));
+    }
+
+    public void unpair(BluetoothAdapter adapter, BluetoothDevice device) {
+        int mask = PAIR_STATE_FLAG;
+        int pairMask = PAIR_STATE_NONE;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("unpair() bluetooth not enabled");
+        }
+
+        int state = device.getBondState();
+        switch (state) {
+            case BluetoothDevice.BOND_BONDED:
+                assertTrue(adapter.getBondedDevices().contains(device));
+                assertTrue(device.removeBond());
+                break;
+            case BluetoothDevice.BOND_BONDING:
+                assertTrue(device.removeBond());
+                break;
+            case BluetoothDevice.BOND_NONE:
+                assertFalse(adapter.getBondedDevices().contains(device));
+                return;
+            default:
+                fail("unpair() invalid state: state=" + state);
+        }
+
+        assertTrue(device.removeBond());
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < UNPAIR_TIMEOUT) {
+            if (device.getBondState() == BluetoothDevice.BOND_NONE) {
+                assertFalse(adapter.getBondedDevices().contains(device));
+                if ((mReceiver.getFiredFlags() & mask) == mask
+                        && (mReceiver.getPairFiredFlags() & pairMask) == pairMask) {
+                    writeOutput(String.format("unpair() completed in %d ms: device=%s",
+                            (System.currentTimeMillis() - s), device));
+                    return;
+                }
+            }
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        int pairFiredFlags = mReceiver.getPairFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("unpair() timeout: state=%d (expected %d), flags=0x%x (expected 0x%x), "
+                + "pairFlags=0x%x (expected 0x%x)", state, BluetoothDevice.BOND_BONDED, firedFlags,
+                mask, pairFiredFlags, pairMask));
+    }
+
+    public void connectA2dp(BluetoothAdapter adapter, BluetoothDevice device) {
+        int mask = PROFILE_A2DP_FLAG;
+        int a2dpMask1 = A2DP_STATE_CONNECTING | A2DP_STATE_CONNECTED | A2DP_STATE_PLAYING;
+        int a2dpMask2 = a2dpMask1 ^ A2DP_STATE_CONNECTED;
+        int a2dpMask3 = a2dpMask1 ^ A2DP_STATE_PLAYING;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("connectA2dp() bluetooth not enabled");
+        }
+
+        if (!adapter.getBondedDevices().contains(device)) {
+            fail("connectA2dp() device not paired: device=" + device);
+        }
+
+        int state = mA2dp.getSinkState(device);
+        switch (state) {
+            case BluetoothA2dp.STATE_CONNECTED:
+            case BluetoothA2dp.STATE_PLAYING:
+                assertTrue(mA2dp.isSinkConnected(device));
+                return;
+            case BluetoothA2dp.STATE_DISCONNECTING:
+            case BluetoothA2dp.STATE_DISCONNECTED:
+                assertFalse(mA2dp.isSinkConnected(device));
+                assertTrue(mA2dp.connectSink(device));
+                break;
+            case BluetoothA2dp.STATE_CONNECTING:
+                assertFalse(mA2dp.isSinkConnected(device));
+                // Don't check for received intents since we might have missed them.
+                mask = a2dpMask1 = a2dpMask2 = a2dpMask3 = 0;
+                break;
+            default:
+                fail("connectA2dp() invalid state: state=" + state);
+        }
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < CONNECT_A2DP_TIMEOUT) {
+            state = mA2dp.getSinkState(device);
+            if (state == BluetoothA2dp.STATE_CONNECTED || state == BluetoothA2dp.STATE_PLAYING) {
+                assertTrue(mA2dp.isSinkConnected(device));
+                // Check whether STATE_CONNECTING and (STATE_CONNECTED or STATE_PLAYING) intents
+                // have fired if we are checking if intents should be fired.
+                int firedFlags = mReceiver.getFiredFlags();
+                int a2dpFiredFlags = mReceiver.getA2dpFiredFlags();
+                if ((mReceiver.getFiredFlags() & mask) == mask
+                        && ((a2dpFiredFlags & a2dpMask1) == a2dpMask1
+                                || (a2dpFiredFlags & a2dpMask2) == a2dpMask2
+                                || (a2dpFiredFlags & a2dpMask3) == a2dpMask3)) {
+                    mReceiver.resetFiredFlags();
+                    writeOutput(String.format("connectA2dp() completed in %d ms: device=%s",
+                            (System.currentTimeMillis() - s), device));
+                    return;
+                }
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        int a2dpFiredFlags = mReceiver.getA2dpFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("connectA2dp() timeout: state=%d (expected %d or %d), "
+                + "flags=0x%x (expected 0x%x), a2dpFlags=0x%x (expected 0x%x or 0x%x or 0x%x)",
+                state, BluetoothHeadset.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING, firedFlags,
+                mask, a2dpFiredFlags, a2dpMask1, a2dpMask2, a2dpMask3));
+    }
+
+    public void disconnectA2dp(BluetoothAdapter adapter, BluetoothDevice device) {
+        int mask = PROFILE_A2DP_FLAG;
+        int a2dpMask = A2DP_STATE_DISCONNECTING | A2DP_STATE_DISCONNECTED;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("disconnectA2dp() bluetooth not enabled");
+        }
+
+        if (!adapter.getBondedDevices().contains(device)) {
+            fail("disconnectA2dp() device not paired: device=" + device);
+        }
+
+        int state = mA2dp.getSinkState(device);
+        switch (state) {
+            case BluetoothA2dp.STATE_DISCONNECTED:
+                assertFalse(mA2dp.isSinkConnected(device));
+                return;
+            case BluetoothA2dp.STATE_CONNECTED:
+            case BluetoothA2dp.STATE_PLAYING:
+                assertTrue(mA2dp.isSinkConnected(device));
+                assertTrue(mA2dp.disconnectSink(device));
+                break;
+            case BluetoothA2dp.STATE_CONNECTING:
+                assertFalse(mA2dp.isSinkConnected(device));
+                assertTrue(mA2dp.disconnectSink(device));
+                break;
+            case BluetoothA2dp.STATE_DISCONNECTING:
+                assertFalse(mA2dp.isSinkConnected(device));
+                // Don't check for received intents since we might have missed them.
+                mask = a2dpMask = 0;
+                break;
+            default:
+                fail("disconnectA2dp() invalid state: state=" + state);
+        }
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < DISCONNECT_A2DP_TIMEOUT) {
+            state = mA2dp.getSinkState(device);
+            if (state == BluetoothA2dp.STATE_DISCONNECTED) {
+                assertFalse(mA2dp.isSinkConnected(device));
+                if ((mReceiver.getFiredFlags() & mask) == mask
+                        && (mReceiver.getA2dpFiredFlags() & a2dpMask) == a2dpMask) {
+                    mReceiver.resetFiredFlags();
+                    writeOutput(String.format("disconnectA2dp() completed in %d ms: device=%s",
+                            (System.currentTimeMillis() - s), device));
+                    return;
+                }
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        int a2dpFiredFlags = mReceiver.getA2dpFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("disconnectA2dp() timeout: state=%d (expected %d), "
+                + "flags=0x%x (expected 0x%x), a2dpFlags=0x%x (expected 0x%x)", state,
+                BluetoothA2dp.STATE_DISCONNECTED, firedFlags, mask, a2dpFiredFlags, a2dpMask));
+    }
+
+    public void connectHeadset(BluetoothAdapter adapter, BluetoothDevice device) {
+        int mask = PROFILE_HEADSET_FLAG;
+        int headsetMask = HEADSET_STATE_CONNECTING | HEADSET_STATE_CONNECTED;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("connectHeadset() bluetooth not enabled");
+        }
+
+        if (!adapter.getBondedDevices().contains(device)) {
+            fail("connectHeadset() device not paired: device=" + device);
+        }
+
+        while (!mHeadsetServiceListener.isConnected()) {
+            sleep(POLL_TIME);
+        }
+
+        int state = mHeadset.getState(device);
+        switch (state) {
+            case BluetoothHeadset.STATE_CONNECTED:
+                assertTrue(mHeadset.isConnected(device));
+                return;
+            case BluetoothHeadset.STATE_DISCONNECTED:
+                assertFalse(mHeadset.isConnected(device));
+                mHeadset.connectHeadset(device);
+                break;
+            case BluetoothHeadset.STATE_CONNECTING:
+                assertFalse(mHeadset.isConnected(device));
+                // Don't check for received intents since we might have missed them.
+                mask = headsetMask = 0;
+                break;
+            case BluetoothHeadset.STATE_ERROR:
+                fail("connectHeadset() error state");
+                break;
+            default:
+                fail("connectHeadset() invalid state: state=" + state);
+        }
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < CONNECT_HEADSET_TIMEOUT) {
+            state = mHeadset.getState(device);
+            if (state == BluetoothHeadset.STATE_CONNECTED) {
+                assertTrue(mHeadset.isConnected(device));
+                if ((mReceiver.getFiredFlags() & mask) == mask
+                        && (mReceiver.getHeadsetFiredFlags() & headsetMask) == headsetMask) {
+                    mReceiver.resetFiredFlags();
+                    writeOutput(String.format("connectHeadset() completed in %d ms: device=%s",
+                            (System.currentTimeMillis() - s), device));
+                    return;
+                }
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        int headsetFiredFlags = mReceiver.getHeadsetFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("connectHeadset() timeout: state=%d (expected %d), "
+                + "flags=0x%x (expected 0x%x), headsetFlags=0x%s (expected 0x%x)", state,
+                BluetoothHeadset.STATE_CONNECTED, firedFlags, mask, headsetFiredFlags,
+                headsetMask));
+    }
+
+    public void disconnectHeadset(BluetoothAdapter adapter, BluetoothDevice device) {
+        int mask = PROFILE_HEADSET_FLAG;
+        int headsetMask = HEADSET_STATE_DISCONNECTED;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("disconnectHeadset() bluetooth not enabled");
+        }
+
+        if (!adapter.getBondedDevices().contains(device)) {
+            fail("disconnectHeadset() device not paired: device=" + device);
+        }
+
+        while (!mHeadsetServiceListener.isConnected()) {
+            sleep(POLL_TIME);
+        }
+
+        int state = mHeadset.getState(device);
+        switch (state) {
+            case BluetoothHeadset.STATE_CONNECTED:
+                mHeadset.disconnectHeadset(device);
+                break;
+            case BluetoothHeadset.STATE_CONNECTING:
+                mHeadset.disconnectHeadset(device);
+                break;
+            case BluetoothHeadset.STATE_DISCONNECTED:
+                return;
+            case BluetoothHeadset.STATE_ERROR:
+                fail("disconnectHeadset() error state");
+                break;
+            default:
+                fail("disconnectHeadset() invalid state: state=" + state);
+        }
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < DISCONNECT_HEADSET_TIMEOUT) {
+            state = mHeadset.getState(device);
+            if (state == BluetoothHeadset.STATE_DISCONNECTED) {
+                assertFalse(mHeadset.isConnected(device));
+                if ((mReceiver.getFiredFlags() & mask) == mask
+                        && (mReceiver.getHeadsetFiredFlags() & headsetMask) == headsetMask) {
+                    mReceiver.resetFiredFlags();
+                    writeOutput(String.format("disconnectHeadset() completed in %d ms: device=%s",
+                            (System.currentTimeMillis() - s), device));
+                    return;
+                }
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        int headsetFiredFlags = mReceiver.getHeadsetFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail(String.format("disconnectHeadset() timeout: state=%d (expected %d), "
+                + "flags=0x%x (expected 0x%x), headsetFlags=0x%s (expected 0x%x)", state,
+                BluetoothHeadset.STATE_DISCONNECTED, firedFlags, mask, headsetFiredFlags,
+                headsetMask));
+    }
+
+    public void writeOutput(String s) {
+        Log.i(mTag, s);
+        if (mOutputWriter == null) {
+            return;
+        }
+        try {
+            mOutputWriter.write(s + "\n");
+            mOutputWriter.flush();
+        } catch (IOException e) {
+            Log.w(mTag, "Could not write to output file", e);
+        }
+    }
+
+    private void sleep(long time) {
+        try {
+            Thread.sleep(time);
+        } catch (InterruptedException e) {
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifier.java b/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifier.java
deleted file mode 100644
index b9e9875..0000000
--- a/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifier.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2009 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.VCardConfig;
-import android.pim.vcard.VCardEntry;
-import android.pim.vcard.VCardEntryConstructor;
-import android.pim.vcard.VCardEntryHandler;
-import android.pim.vcard.VCardParser;
-import android.pim.vcard.VCardParser_V21;
-import android.pim.vcard.VCardParser_V30;
-import android.pim.vcard.exception.VCardException;
-import android.test.AndroidTestCase;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-
-/* package */ class ContentValuesVerifier implements VCardEntryHandler {
-    private AndroidTestCase mTestCase;
-    private List<ContentValuesVerifierElem> mContentValuesVerifierElemList =
-        new ArrayList<ContentValuesVerifierElem>();
-    private int mIndex;
-
-    public ContentValuesVerifierElem addElem(AndroidTestCase androidTestCase) {
-        mTestCase = androidTestCase;
-        ContentValuesVerifierElem importVerifier = new ContentValuesVerifierElem(androidTestCase);
-        mContentValuesVerifierElemList.add(importVerifier);
-        return importVerifier;
-    }
-
-    public void verify(int resId, int vCardType) throws IOException, VCardException {
-        verify(mTestCase.getContext().getResources().openRawResource(resId), vCardType);
-    }
-
-    public void verify(int resId, int vCardType, final VCardParser vCardParser)
-            throws IOException, VCardException {
-        verify(mTestCase.getContext().getResources().openRawResource(resId),
-                vCardType, vCardParser);
-    }
-
-    public void verify(InputStream is, int vCardType) throws IOException, VCardException {
-        final VCardParser vCardParser;
-        if (VCardConfig.isV30(vCardType)) {
-            vCardParser = new VCardParser_V30(true);  // use StrictParsing
-        } else {
-            vCardParser = new VCardParser_V21();
-        }
-        verify(is, vCardType, vCardParser);
-    }
-
-    public void verify(InputStream is, int vCardType, final VCardParser vCardParser)
-            throws IOException, VCardException {
-        VCardEntryConstructor builder =
-            new VCardEntryConstructor(null, null, false, vCardType, null);
-        builder.addEntryHandler(this);
-        try {
-            vCardParser.parse(is, builder);
-        } finally {
-            if (is != null) {
-                try {
-                    is.close();
-                } catch (IOException e) {
-                }
-            }
-        }
-    }
-
-    public void onStart() {
-        for (ContentValuesVerifierElem elem : mContentValuesVerifierElemList) {
-            elem.onParsingStart();
-        }
-    }
-
-    public void onEntryCreated(VCardEntry entry) {
-        mTestCase.assertTrue(mIndex < mContentValuesVerifierElemList.size());
-        mContentValuesVerifierElemList.get(mIndex).onEntryCreated(entry);
-        mIndex++;
-    }
-
-    public void onEnd() {
-        for (ContentValuesVerifierElem elem : mContentValuesVerifierElemList) {
-            elem.onParsingEnd();
-            elem.verifyResolver();
-        }
-    }
-}
diff --git a/core/tests/coretests/src/android/pim/vcard/ExportTestResolver.java b/core/tests/coretests/src/android/pim/vcard/ExportTestResolver.java
deleted file mode 100644
index 951d0d8..0000000
--- a/core/tests/coretests/src/android/pim/vcard/ExportTestResolver.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (C) 2009 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.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Entity;
-import android.content.EntityIterator;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.RawContacts;
-import android.test.mock.MockContentProvider;
-import android.test.mock.MockContentResolver;
-import android.test.mock.MockCursor;
-
-import junit.framework.TestCase;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-/* package */ public class ExportTestResolver extends MockContentResolver {
-    ExportTestProvider mProvider;
-    public ExportTestResolver(TestCase testCase) {
-        mProvider = new ExportTestProvider(testCase);
-        addProvider(VCardComposer.VCARD_TEST_AUTHORITY, mProvider);
-        addProvider(RawContacts.CONTENT_URI.getAuthority(), mProvider);
-    }
-
-    public ContactEntry addInputContactEntry() {
-        return mProvider.buildInputEntry();
-    }
-
-    public ExportTestProvider getProvider() {
-        return mProvider;
-    }
-}
-
-/* package */ class MockEntityIterator implements EntityIterator {
-    List<Entity> mEntityList;
-    Iterator<Entity> mIterator;
-
-    public MockEntityIterator(List<ContentValues> contentValuesList) {
-        mEntityList = new ArrayList<Entity>();
-        Entity entity = new Entity(new ContentValues());
-        for (ContentValues contentValues : contentValuesList) {
-                entity.addSubValue(Data.CONTENT_URI, contentValues);
-        }
-        mEntityList.add(entity);
-        mIterator = mEntityList.iterator();
-    }
-
-    public boolean hasNext() {
-        return mIterator.hasNext();
-    }
-
-    public Entity next() {
-        return mIterator.next();
-    }
-
-    public void remove() {
-        throw new UnsupportedOperationException("remove not supported");
-    }
-
-    public void reset() {
-        mIterator = mEntityList.iterator();
-    }
-
-    public void close() {
-    }
-}
-
-/**
- * Represents one contact, which should contain multiple ContentValues like
- * StructuredName, Email, etc.
- */
-/* package */ class ContactEntry {
-    private final List<ContentValues> mContentValuesList = new ArrayList<ContentValues>();
-
-    public ContentValuesBuilder addContentValues(String mimeType) {
-        ContentValues contentValues = new ContentValues();
-        contentValues.put(Data.MIMETYPE, mimeType);
-        mContentValuesList.add(contentValues);
-        return new ContentValuesBuilder(contentValues);
-    }
-
-    public List<ContentValues> getList() {
-        return mContentValuesList;
-    }
-}
-
-/* package */ class ExportTestProvider extends MockContentProvider {
-    final private TestCase mTestCase;
-    final private ArrayList<ContactEntry> mContactEntryList = new ArrayList<ContactEntry>();
-
-    public ExportTestProvider(TestCase testCase) {
-        mTestCase = testCase;
-    }
-
-    public ContactEntry buildInputEntry() {
-        ContactEntry contactEntry = new ContactEntry();
-        mContactEntryList.add(contactEntry);
-        return contactEntry;
-    }
-
-    /**
-     * <p>
-     * An old method which had existed but was removed from ContentResolver.
-     * </p>
-     * <p>
-     * We still keep using this method since we don't have a propeer way to know
-     * which value in the ContentValue corresponds to the entry in Contacts database.
-     * </p>
-     * <p>
-     * Detail:
-     * There's an easy way to know which index "family name" corresponds to, via
-     * {@link android.provider.ContactsContract}.
-     * FAMILY_NAME equals DATA3, so the corresponding index
-     * for "family name" should be 2 (note that index is 0-origin).
-     * However, we cannot know what the index 2 corresponds to; it may be "family name",
-     * "label" for now, but may be the other some column in the future. We don't have
-     * convenient way to know the original data structure.
-     * </p>
-     */
-    public EntityIterator queryEntities(Uri uri,
-            String selection, String[] selectionArgs, String sortOrder) {
-        mTestCase.assertTrue(uri != null);
-        mTestCase.assertTrue(ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()));
-        final String authority = uri.getAuthority();
-        mTestCase.assertTrue(RawContacts.CONTENT_URI.getAuthority().equals(authority));
-        mTestCase.assertTrue((Data.CONTACT_ID + "=?").equals(selection));
-        mTestCase.assertEquals(1, selectionArgs.length);
-        final int id = Integer.parseInt(selectionArgs[0]);
-        mTestCase.assertTrue(id >= 0 && id < mContactEntryList.size());
-
-        return new MockEntityIterator(mContactEntryList.get(id).getList());
-    }
-
-    @Override
-    public Cursor query(Uri uri,String[] projection,
-            String selection, String[] selectionArgs, String sortOrder) {
-        mTestCase.assertTrue(VCardComposer.CONTACTS_TEST_CONTENT_URI.equals(uri));
-        // In this test, following arguments are not supported.
-        mTestCase.assertNull(selection);
-        mTestCase.assertNull(selectionArgs);
-        mTestCase.assertNull(sortOrder);
-
-        return new MockCursor() {
-            int mCurrentPosition = -1;
-
-            @Override
-            public int getCount() {
-                return mContactEntryList.size();
-            }
-
-            @Override
-            public boolean moveToFirst() {
-                mCurrentPosition = 0;
-                return true;
-            }
-
-            @Override
-            public boolean moveToNext() {
-                if (mCurrentPosition < mContactEntryList.size()) {
-                    mCurrentPosition++;
-                    return true;
-                } else {
-                    return false;
-                }
-            }
-
-            @Override
-            public boolean isBeforeFirst() {
-                return mCurrentPosition < 0;
-            }
-
-            @Override
-            public boolean isAfterLast() {
-                return mCurrentPosition >= mContactEntryList.size();
-            }
-
-            @Override
-            public int getColumnIndex(String columnName) {
-                mTestCase.assertEquals(Contacts._ID, columnName);
-                return 0;
-            }
-
-            @Override
-            public int getInt(int columnIndex) {
-                mTestCase.assertEquals(0, columnIndex);
-                mTestCase.assertTrue(mCurrentPosition >= 0
-                        && mCurrentPosition < mContactEntryList.size());
-                return mCurrentPosition;
-            }
-
-            @Override
-            public String getString(int columnIndex) {
-                return String.valueOf(getInt(columnIndex));
-            }
-
-            @Override
-            public void close() {
-            }
-        };
-    }
-}
diff --git a/core/tests/coretests/src/android/pim/vcard/VCardExporterTests.java b/core/tests/coretests/src/android/pim/vcard/VCardExporterTests.java
index 2962a926..2bec462 100644
--- a/core/tests/coretests/src/android/pim/vcard/VCardExporterTests.java
+++ b/core/tests/coretests/src/android/pim/vcard/VCardExporterTests.java
@@ -17,7 +17,10 @@
 package android.pim.vcard;
 
 import android.content.ContentValues;
-import android.pim.vcard.VCardConfig;
+import android.pim.vcard.test_utils.ContactEntry;
+import android.pim.vcard.test_utils.PropertyNodesVerifierElem;
+import android.pim.vcard.test_utils.PropertyNodesVerifierElem.TypeSet;
+import android.pim.vcard.test_utils.VCardTestsBase;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Event;
 import android.provider.ContactsContract.CommonDataKinds.Im;
@@ -31,8 +34,6 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
 
-import android.pim.vcard.PropertyNodesVerifierElem.TypeSet;
-
 import java.util.Arrays;
 
 /**
@@ -56,7 +57,6 @@
     }
 
     private void testStructuredNameBasic(int vcardType) {
-        final boolean isV30 = VCardConfig.isV30(vcardType);
         mVerifier.initForExportTest(vcardType);
         mVerifier.addInputEntry().addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName")
@@ -64,28 +64,15 @@
                 .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName")
                 .put(StructuredName.PREFIX, "AppropriatePrefix")
                 .put(StructuredName.SUFFIX, "AppropriateSuffix")
-                .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily")
-                .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven")
-                .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle");
+                .put(StructuredName.DISPLAY_NAME, "DISPLAY NAME");
 
-        PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem()
+        mVerifier.addPropertyNodesVerifierElem()
                 .addExpectedNodeWithOrder("N",
                         "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;"
                         + "AppropriatePrefix;AppropriateSuffix",
                         Arrays.asList("AppropriateFamilyName", "AppropriateGivenName",
                                 "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix"))
-                .addExpectedNodeWithOrder("FN",
-                        "AppropriatePrefix AppropriateGivenName "
-                        + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix")
-                .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven")
-                .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle")
-                .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily");
-
-        if (isV30) {
-            elem.addExpectedNode("SORT-STRING",
-                    "AppropriatePhoneticGiven AppropriatePhoneticMiddle "
-                    + "AppropriatePhoneticFamily");
-        }
+                .addExpectedNodeWithOrder("FN", "DISPLAY NAME");
     }
 
     public void testStructuredNameBasicV21() {
@@ -96,6 +83,10 @@
         testStructuredNameBasic(V30);
     }
 
+    public void testStructuredNameBasicV40() {
+        testStructuredNameBasic(V40);
+    }
+
     /**
      * Test that only "primary" StructuredName is emitted, so that our vCard file
      * will not confuse the external importer, assuming there may be some importer
@@ -103,18 +94,15 @@
      * Note that more than one "N", "FN", etc. properties are acceptable in vCard spec.
      */
     private void testStructuredNameUsePrimaryCommon(int vcardType) {
-        final boolean isV30 = (vcardType == V30);
         mVerifier.initForExportTest(vcardType);
-        ContactEntry entry = mVerifier.addInputEntry();
+        final ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName1")
                 .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName1")
                 .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName1")
                 .put(StructuredName.PREFIX, "DoNotEmitPrefix1")
                 .put(StructuredName.SUFFIX, "DoNotEmitSuffix1")
-                .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily1")
-                .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven1")
-                .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle1");
+                .put(StructuredName.DISPLAY_NAME, "DoNotEmitDisplayName1");
 
         // With "IS_PRIMARY=1". This is what we should use.
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
@@ -123,41 +111,30 @@
                 .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName")
                 .put(StructuredName.PREFIX, "AppropriatePrefix")
                 .put(StructuredName.SUFFIX, "AppropriateSuffix")
-                .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily")
-                .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven")
-                .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle")
+                .put(StructuredName.DISPLAY_NAME, "AppropriateDisplayName")
                 .put(StructuredName.IS_PRIMARY, 1);
 
         // With "IS_PRIMARY=1", but we should ignore this time, since this is second, not first.
+        // vCard 2.1 does not specify anything about the number of N properties. We choose not
+        // emitting this property.
+        // vCard 3.0 does (There must be one N property)
+        // vCard 4.0 (rev13) does (cardinality (0, 1)).
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName2")
                 .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName2")
                 .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName2")
                 .put(StructuredName.PREFIX, "DoNotEmitPrefix2")
                 .put(StructuredName.SUFFIX, "DoNotEmitSuffix2")
-                .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily2")
-                .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven2")
-                .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle2")
+                .put(StructuredName.DISPLAY_NAME, "DoNotEmitDisplayName2")
                 .put(StructuredName.IS_PRIMARY, 1);
 
-        PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem()
+       mVerifier.addPropertyNodesVerifierElem()
                 .addExpectedNodeWithOrder("N",
                         "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;"
                         + "AppropriatePrefix;AppropriateSuffix",
                         Arrays.asList("AppropriateFamilyName", "AppropriateGivenName",
                                 "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix"))
-                .addExpectedNodeWithOrder("FN",
-                        "AppropriatePrefix AppropriateGivenName "
-                        + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix")
-                .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven")
-                .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle")
-                .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily");
-
-        if (isV30) {
-            elem.addExpectedNode("SORT-STRING",
-                    "AppropriatePhoneticGiven AppropriatePhoneticMiddle "
-                    + "AppropriatePhoneticFamily");
-        }
+                .addExpectedNodeWithOrder("FN", "AppropriateDisplayName");
     }
 
     public void testStructuredNameUsePrimaryV21() {
@@ -168,14 +145,152 @@
         testStructuredNameUsePrimaryCommon(V30);
     }
 
+    public void testStructuredNameUsePrimaryV40() {
+        testStructuredNameUsePrimaryCommon(V40);
+    }
+
     /**
      * Tests that only "super primary" StructuredName is emitted.
      * See also the comment in {@link #testStructuredNameUsePrimaryCommon(int)}.
      */
     private void testStructuredNameUseSuperPrimaryCommon(int vcardType) {
-        final boolean isV30 = (vcardType == V30);
         mVerifier.initForExportTest(vcardType);
-        ContactEntry entry = mVerifier.addInputEntry();
+        final ContactEntry entry = mVerifier.addInputEntry();
+        entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
+                .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName1")
+                .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName1")
+                .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName1")
+                .put(StructuredName.PREFIX, "DoNotEmitPrefix1")
+                .put(StructuredName.SUFFIX, "DoNotEmitSuffix1")
+                .put(StructuredName.DISPLAY_NAME, "DoNotEmitDisplay1");
+
+        // With "IS_PRIMARY=1", but we should ignore this time.
+        entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
+                .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName2")
+                .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName2")
+                .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName2")
+                .put(StructuredName.PREFIX, "DoNotEmitPrefix2")
+                .put(StructuredName.SUFFIX, "DoNotEmitSuffix2")
+                .put(StructuredName.DISPLAY_NAME, "DoNotEmitDisplay2")
+                .put(StructuredName.IS_PRIMARY, 1);
+
+        // With "IS_SUPER_PRIMARY=1". This is what we should use.
+        entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
+                .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName")
+                .put(StructuredName.GIVEN_NAME, "AppropriateGivenName")
+                .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName")
+                .put(StructuredName.PREFIX, "AppropriatePrefix")
+                .put(StructuredName.SUFFIX, "AppropriateSuffix")
+                .put(StructuredName.DISPLAY_NAME, "AppropriateDisplayName")
+                .put(StructuredName.IS_SUPER_PRIMARY, 1);
+
+        entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
+                .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName3")
+                .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName3")
+                .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName3")
+                .put(StructuredName.PREFIX, "DoNotEmitPrefix3")
+                .put(StructuredName.SUFFIX, "DoNotEmitSuffix3")
+                .put(StructuredName.DISPLAY_NAME, "DoNotEmitDisplay3")
+                .put(StructuredName.IS_PRIMARY, 1);
+
+        final PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem();
+        elem.addExpectedNodeWithOrder("N",
+                "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;"
+                + "AppropriatePrefix;AppropriateSuffix",
+                Arrays.asList("AppropriateFamilyName", "AppropriateGivenName",
+                        "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix"));
+
+        elem.addExpectedNodeWithOrder("FN", "AppropriateDisplayName");
+    }
+
+    public void testStructuredNameUseSuperPrimaryV21() {
+        testStructuredNameUseSuperPrimaryCommon(V21);
+    }
+
+    public void testStructuredNameUseSuperPrimaryV30() {
+        testStructuredNameUseSuperPrimaryCommon(V30);
+    }
+
+    public void testStructuredNameUseSuperPrimaryV40() {
+        testStructuredNameUseSuperPrimaryCommon(V40);
+    }
+
+    /**
+     * Tests phonetic names field are handled correctly.
+     *
+     * vCard 2.1 does not have any field corresponding to them.
+     * vCard 3.0 has SORT-STRING property, which does not support multiple values inside it.
+     * vCard 4.0 (rev13) has SORT-AS parameter, which has three values (family, given, middle)
+     * inside it.
+     */
+    private void testStructuredNamePhoneticNameCommon(int vcardType) {
+        mVerifier.initForExportTest(vcardType);
+        final ContactEntry entry = mVerifier.addInputEntry();
+        entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
+                .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName")
+                .put(StructuredName.GIVEN_NAME, "AppropriateGivenName")
+                .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName")
+                .put(StructuredName.PREFIX, "AppropriatePrefix")
+                .put(StructuredName.SUFFIX, "AppropriateSuffix")
+                .put(StructuredName.DISPLAY_NAME, "AppropriateDisplayName")
+                .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily")
+                .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven")
+                .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle");
+
+        final PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem();
+        if (VCardConfig.isVersion40(vcardType)) {
+            final ContentValues contentValues = new ContentValues();
+            contentValues.put("SORT-AS",
+                    "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName");
+            // vCard 4.0 (rev13) now uses SORT-AS parameter, which is not compatible with
+            // either 2.1 nor 3.0.
+            elem.addExpectedNodeWithOrder("N",
+                    "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;"
+                    + "AppropriatePrefix;AppropriateSuffix",
+                    Arrays.asList("AppropriateFamilyName", "AppropriateGivenName",
+                            "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix"),
+                    contentValues);
+        } else {
+            elem.addExpectedNodeWithOrder("N",
+                    "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;"
+                    + "AppropriatePrefix;AppropriateSuffix",
+                    Arrays.asList("AppropriateFamilyName", "AppropriateGivenName",
+                            "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix"));
+            if (VCardConfig.isVersion30(vcardType)) {
+                elem.addExpectedNode("SORT-STRING",
+                        "AppropriatePhoneticGiven AppropriatePhoneticMiddle"
+                        + " AppropriatePhoneticFamily");
+            }
+        }
+
+        elem.addExpectedNodeWithOrder("FN", "AppropriateDisplayName")
+            .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven")
+            .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle")
+            .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily");
+    }
+
+    public void testStructuredNamePhoneticNameV21() {
+        testStructuredNamePhoneticNameCommon(V21);
+    }
+
+    public void testStructuredNamePhoneticNameV30() {
+        testStructuredNamePhoneticNameCommon(V30);
+    }
+
+    public void testStructuredNamePhoneticNameV40() {
+        testStructuredNamePhoneticNameCommon(V40);
+    }
+
+    // TODO: need to add test cases confirming escaping, empty values, etc.
+
+    /**
+     * Confirms all the other sides of the handling is correctly interpreted at one time.
+     *
+     * A kind of regression test for StructuredName handling.
+     */
+    private void testStructuredNameComplicatedCommon(int vcardType) {
+        mVerifier.initForExportTest(vcardType);
+        final ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName1")
                 .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName1")
@@ -221,32 +336,50 @@
                 .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle3")
                 .put(StructuredName.IS_PRIMARY, 1);
 
-        PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("N",
-                        "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;"
-                        + "AppropriatePrefix;AppropriateSuffix",
-                        Arrays.asList("AppropriateFamilyName", "AppropriateGivenName",
-                                "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix"))
-                .addExpectedNodeWithOrder("FN",
-                        "AppropriatePrefix AppropriateGivenName "
-                        + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix")
-                .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven")
-                .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle")
-                .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily");
-
-        if (isV30) {
-            elem.addExpectedNode("SORT-STRING",
-                    "AppropriatePhoneticGiven AppropriatePhoneticMiddle"
-                    + " AppropriatePhoneticFamily");
+        final PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem();
+        if (VCardConfig.isVersion40(vcardType)) {
+            final ContentValues contentValues = new ContentValues();
+            contentValues.put("SORT-AS",
+                    "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName");
+            // vCard 4.0 (rev13) now uses SORT-AS parameter, which is not compatible with
+            // either 2.1 nor 3.0.
+            elem.addExpectedNodeWithOrder("N",
+                    "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;"
+                    + "AppropriatePrefix;AppropriateSuffix",
+                    Arrays.asList("AppropriateFamilyName", "AppropriateGivenName",
+                            "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix"),
+                    contentValues);
+        } else {
+            elem.addExpectedNodeWithOrder("N",
+                    "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;"
+                    + "AppropriatePrefix;AppropriateSuffix",
+                    Arrays.asList("AppropriateFamilyName", "AppropriateGivenName",
+                            "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix"));
+            if (VCardConfig.isVersion30(vcardType)) {
+                elem.addExpectedNode("SORT-STRING",
+                        "AppropriatePhoneticGiven AppropriatePhoneticMiddle"
+                        + " AppropriatePhoneticFamily");
+            }
         }
+
+        elem.addExpectedNodeWithOrder("FN",
+                "AppropriatePrefix AppropriateGivenName "
+                + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix")
+            .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven")
+            .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle")
+            .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily");
     }
 
-    public void testStructuredNameUseSuperPrimaryV21() {
-        testStructuredNameUseSuperPrimaryCommon(V21);
+    public void testStructuredNameComplicatedV21() {
+        testStructuredNameComplicatedCommon(V21);
     }
 
-    public void testStructuredNameUseSuperPrimaryV30() {
-        testStructuredNameUseSuperPrimaryCommon(V30);
+    public void testStructuredNameComplicatedV30() {
+        testStructuredNameComplicatedCommon(V30);
+    }
+
+    public void testStructuredNameComplicatedV40() {
+        testStructuredNameComplicatedCommon(V40);
     }
 
     public void testNickNameV30() {
@@ -258,6 +391,15 @@
             .addExpectedNodeWithOrder("NICKNAME", "Nicky");
     }
 
+    public void testNickNameV40() {
+        mVerifier.initForExportTest(V40);
+        mVerifier.addInputEntry().addContentValues(Nickname.CONTENT_ITEM_TYPE)
+                .put(Nickname.NAME, "Nicky");
+
+        mVerifier.addPropertyNodesVerifierElemWithEmptyName()
+            .addExpectedNodeWithOrder("NICKNAME", "Nicky");
+    }
+
     private void testPhoneBasicCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         mVerifier.addInputEntry().addContentValues(Phone.CONTENT_ITEM_TYPE)
@@ -275,6 +417,10 @@
         testPhoneBasicCommon(V30);
     }
 
+    public void testPhoneBasicV40() {
+        testPhoneBasicCommon(V40);
+    }
+
     public void testPhoneRefrainFormatting() {
         mVerifier.initForExportTest(V21 | VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING);
         mVerifier.addInputEntry().addContentValues(Phone.CONTENT_ITEM_TYPE)
@@ -367,6 +513,10 @@
         testPhoneVariousTypeSupport(V30);
     }
 
+    public void testPhoneVariousTypeSupportV40() {
+        testPhoneVariousTypeSupport(V40);
+    }
+
     /**
      * Tests that "PREF"s are emitted appropriately.
      */
@@ -403,6 +553,10 @@
         testPhonePrefHandlingCommon(V30);
     }
 
+    public void testPhonePrefHandlingV40() {
+        testPhonePrefHandlingCommon(V40);
+    }
+
     private void testMiscPhoneTypeHandling(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         ContactEntry entry = mVerifier.addInputEntry();
@@ -438,7 +592,7 @@
                 .put(Phone.TYPE, Phone.TYPE_CUSTOM)
                 .put(Phone.LABEL, "invalid");
         PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElemWithEmptyName();
-        if (VCardConfig.isV30(vcardType)) {
+        if (VCardConfig.isVersion30(vcardType) || VCardConfig.isVersion40(vcardType)) {
             // vCard 3.0 accepts "invalid". Also stop using toUpper()
             elem.addExpectedNode("TEL", "1", new TypeSet("Modem"))
                     .addExpectedNode("TEL", "2", new TypeSet("MSG"))
@@ -468,6 +622,10 @@
         testMiscPhoneTypeHandling(V30);
     }
 
+    public void testPhoneTypeHandlingV40() {
+        testMiscPhoneTypeHandling(V40);
+    }
+
     private void testEmailBasicCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         mVerifier.addInputEntry().addContentValues(Email.CONTENT_ITEM_TYPE)
@@ -484,6 +642,10 @@
         testEmailBasicCommon(V30);
     }
 
+    public void testEmailBasicV40() {
+        testEmailBasicCommon(V40);
+    }
+
     private void testEmailVariousTypeSupportCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         ContactEntry entry = mVerifier.addInputEntry();
@@ -514,6 +676,10 @@
         testEmailVariousTypeSupportCommon(V30);
     }
 
+    public void testEmailVariousTypeSupportV40() {
+        testEmailVariousTypeSupportCommon(V40);
+    }
+
     private void testEmailPrefHandlingCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         ContactEntry entry = mVerifier.addInputEntry();
@@ -538,6 +704,10 @@
         testEmailPrefHandlingCommon(V30);
     }
 
+    public void testEmailPrefHandlingV40() {
+        testEmailPrefHandlingCommon(V40);
+    }
+
     private void testPostalAddressCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE)
@@ -569,6 +739,10 @@
         testPostalAddressCommon(V30);
     }
 
+    public void testPostalAddressV40() {
+        testPostalAddressCommon(V40);
+    }
+
     private void testPostalAddressNonNeighborhood(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE)
@@ -586,6 +760,10 @@
         testPostalAddressNonNeighborhood(V30);
     }
 
+    public void testPostalAddressNonNeighborhoodV40() {
+        testPostalAddressNonNeighborhood(V40);
+    }
+
     private void testPostalAddressNonCity(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE)
@@ -603,6 +781,10 @@
         testPostalAddressNonCity(V30);
     }
 
+    public void testPostalAddressNonCityV40() {
+        testPostalAddressNonCity(V40);
+    }
+
     private void testPostalOnlyWithFormattedAddressCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE)
@@ -623,6 +805,10 @@
         testPostalOnlyWithFormattedAddressCommon(V30);
     }
 
+    public void testPostalOnlyWithFormattedAddressV40() {
+        testPostalOnlyWithFormattedAddressCommon(V40);
+    }
+
     /**
      * Tests that the vCard composer honors formatted data when it is available
      * even when it is partial.
@@ -648,6 +834,10 @@
         testPostalWithBothStructuredAndFormattedCommon(V30);
     }
 
+    public void testPostalWithBothStructuredAndFormattedV40() {
+        testPostalWithBothStructuredAndFormattedCommon(V40);
+    }
+
     private void testOrganizationCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         ContactEntry entry = mVerifier.addInputEntry();
@@ -685,6 +875,10 @@
         testOrganizationCommon(V30);
     }
 
+    public void testOrganizationV40() {
+        testOrganizationCommon(V40);
+    }
+
     private void testImVariousTypeSupportCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         ContactEntry entry = mVerifier.addInputEntry();
@@ -737,6 +931,10 @@
         testImVariousTypeSupportCommon(V30);
     }
 
+    public void testImBasicV40() {
+        testImVariousTypeSupportCommon(V40);
+    }
+
     private void testImPrefHandlingCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         ContactEntry entry = mVerifier.addInputEntry();
@@ -762,6 +960,10 @@
         testImPrefHandlingCommon(V30);
     }
 
+    public void testImPrefHandlingV40() {
+        testImPrefHandlingCommon(V40);
+    }
+
     private void testWebsiteCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         ContactEntry entry = mVerifier.addInputEntry();
@@ -786,6 +988,10 @@
         testWebsiteCommon(V30);
     }
 
+    public void testWebsiteV40() {
+        testWebsiteCommon(V40);
+    }
+
     private String getAndroidPropValue(final String mimeType, String value, Integer type) {
         return getAndroidPropValue(mimeType, value, type, null);
     }
@@ -839,6 +1045,10 @@
         testEventCommon(V30);
     }
 
+    public void testEventV40() {
+        testEventCommon(V40);
+    }
+
     private void testNoteCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         ContactEntry entry = mVerifier.addInputEntry();
@@ -860,8 +1070,13 @@
         testNoteCommon(V30);
     }
 
+    public void testNoteV40() {
+        testNoteCommon(V40);
+    }
+
     private void testPhotoCommon(int vcardType) {
-        final boolean isV30 = vcardType == V30;
+        final boolean useB =
+            (VCardConfig.isVersion30(vcardType) || VCardConfig.isVersion40(vcardType));
         mVerifier.initForExportTest(vcardType);
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
@@ -870,7 +1085,7 @@
                 .put(Photo.PHOTO, sPhotoByteArray);
 
         ContentValues contentValuesForPhoto = new ContentValues();
-        contentValuesForPhoto.put("ENCODING", (isV30 ? "b" : "BASE64"));
+        contentValuesForPhoto.put("ENCODING", (useB ? "b" : "BASE64"));
         mVerifier.addPropertyNodesVerifierElem()
                 .addExpectedNode("FN", "PhotoTest")
                 .addExpectedNode("N", "PhotoTest;;;;",
@@ -887,6 +1102,10 @@
         testPhotoCommon(V30);
     }
 
+    public void testPhotoV40() {
+        testPhotoCommon(V40);
+    }
+
     private void testRelationCommon(int vcardType) {
         mVerifier.initForExportTest(vcardType);
         mVerifier.addInputEntry().addContentValues(Relation.CONTENT_ITEM_TYPE)
@@ -957,18 +1176,22 @@
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.IS_PRIMARY, 1);  // Empty name. Should be ignored.
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
-                .put(StructuredName.FAMILY_NAME, "family1");  // Not primary. Should be ignored.
+                .put(StructuredName.FAMILY_NAME, "family1")  // Not primary. Should be ignored.
+                .put(StructuredName.DISPLAY_NAME, "display");
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.IS_PRIMARY, 1)
-                .put(StructuredName.FAMILY_NAME, "family2");  // This entry is what we want.
+                .put(StructuredName.FAMILY_NAME, "family2")  // This entry is what we want.
+                .put(StructuredName.DISPLAY_NAME, "display");
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.IS_PRIMARY, 1)
-                .put(StructuredName.FAMILY_NAME, "family3");
+                .put(StructuredName.FAMILY_NAME, "family3")
+                .put(StructuredName.DISPLAY_NAME, "display");
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
-                .put(StructuredName.FAMILY_NAME, "family4");
+                .put(StructuredName.FAMILY_NAME, "family4")
+                .put(StructuredName.DISPLAY_NAME, "display");
         mVerifier.addPropertyNodesVerifierElem()
                 .addExpectedNode("N", Arrays.asList("family2", "", "", "", ""))
-                .addExpectedNode("FN", "family2");
+                .addExpectedNode("FN", "display");
     }
 
     public void testPickUpNonEmptyContentValuesV21() {
@@ -978,4 +1201,23 @@
     public void testPickUpNonEmptyContentValuesV30() {
         testPickUpNonEmptyContentValuesCommon(V30);
     }
+
+    public void testPickUpNonEmptyContentValuesV40() {
+        testPickUpNonEmptyContentValuesCommon(V40);
+    }
+
+    public void testUseMultiByteTypeV30() {
+        mVerifier.initForExportTest(V30);
+        final ContactEntry entry = mVerifier.addInputEntry();
+        entry.addContentValues(Phone.CONTENT_ITEM_TYPE)
+                .put(Phone.TYPE, Phone.TYPE_CUSTOM)
+                .put(Phone.LABEL, "\u96FB\u8A71")
+                .put(Phone.NUMBER, "1");
+        mVerifier.addLineVerifierElem()
+                .addExpected("N:")
+                .addExpected("FN:")
+                .addExpected("TEL;TYPE=\u96FB\u8A71:1");
+        mVerifier.addPropertyNodesVerifierElemWithEmptyName()
+                .addExpectedNode("TEL", "1", new TypeSet("\u96FB\u8A71"));
+    }
 }
diff --git a/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java b/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java
index e0e1f87..09e2914 100644
--- a/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java
+++ b/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java
@@ -15,11 +15,17 @@
  */
 package android.pim.vcard;
 
+import com.android.frameworks.coretests.R;
+
 import android.content.ContentValues;
-import android.pim.vcard.VCardConfig;
-import android.provider.ContactsContract.Data;
+import android.pim.vcard.test_utils.PropertyNodesVerifierElem.TypeSet;
+import android.pim.vcard.test_utils.VCardTestsBase;
+import android.pim.vcard.test_utils.ContentValuesVerifier;
+import android.pim.vcard.test_utils.ContentValuesVerifierElem;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
 import android.provider.ContactsContract.CommonDataKinds.Note;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -27,9 +33,7 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
-
-import com.android.frameworks.coretests.R;
-import android.pim.vcard.PropertyNodesVerifierElem.TypeSet;
+import android.provider.ContactsContract.Data;
 
 import java.util.Arrays;
 
@@ -405,12 +409,12 @@
 
     public void testV21SimpleCase1_Parsing() {
         mVerifier.initForImportTest(V21, R.raw.v21_simple_1);
-        mVerifier.addPropertyNodesVerifierElem()
+        mVerifier.addPropertyNodesVerifierElemWithoutVersion()  // no "VERSION:2.1" line.
                 .addExpectedNodeWithOrder("N", "Ando;Roid;", Arrays.asList("Ando", "Roid", ""));
     }
 
     public void testV21SimpleCase1_Type_Generic() {
-        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, R.raw.v21_simple_1);
+        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_GENERIC, R.raw.v21_simple_1);
         mVerifier.addContentValuesVerifierElem()
                 .addExpected(StructuredName.CONTENT_ITEM_TYPE)
                         .put(StructuredName.FAMILY_NAME, "Ando")
@@ -419,7 +423,7 @@
     }
 
     public void testV21SimpleCase1_Type_Japanese() {
-        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, R.raw.v21_simple_1);
+        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_simple_1);
         mVerifier.addContentValuesVerifierElem()
                 .addExpected(StructuredName.CONTENT_ITEM_TYPE)
                         .put(StructuredName.FAMILY_NAME, "Ando")
@@ -431,7 +435,7 @@
     }
 
     public void testV21SimpleCase2() {
-        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, R.raw.v21_simple_2);
+        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_simple_2);
         mVerifier.addContentValuesVerifierElem()
                 .addExpected(StructuredName.CONTENT_ITEM_TYPE)
                         .put(StructuredName.DISPLAY_NAME, "Ando Roid");
@@ -454,7 +458,6 @@
     public void testV21BackslashCase_Parsing() {
         mVerifier.initForImportTest(V21, R.raw.v21_backslash);
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "2.1")
                 .addExpectedNodeWithOrder("N", ";A;B\\;C\\;;D;:E;\\\\;",
                         Arrays.asList("", "A;B\\", "C\\;", "D", ":E", "\\\\", ""))
                 .addExpectedNodeWithOrder("FN", "A;B\\C\\;D:E\\\\");
@@ -549,11 +552,11 @@
     public void testV21ComplicatedCase_Parsing() {
         mVerifier.initForImportTest(V21, R.raw.v21_complicated);
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "2.1")
                 .addExpectedNodeWithOrder("N", "Gump;Forrest;Hoge;Pos;Tao",
                         Arrays.asList("Gump", "Forrest", "Hoge", "Pos", "Tao"))
                 .addExpectedNodeWithOrder("FN", "Joe Due")
-                .addExpectedNodeWithOrder("ORG", "Gump Shrimp Co.;Sales Dept.;Manager;Fish keeper",
+                .addExpectedNodeWithOrder("ORG",
+                        "Gump Shrimp Co.;Sales Dept.;Manager;Fish keeper",
                         Arrays.asList("Gump Shrimp Co.", "Sales Dept.;Manager", "Fish keeper"))
                 .addExpectedNodeWithOrder("ROLE", "Fish Cake Keeper!")
                 .addExpectedNodeWithOrder("TITLE", "Shrimp Man")
@@ -583,7 +586,8 @@
                 .addExpectedNodeWithOrder("EMAIL", "forrestgump@walladalla.com",
                         new TypeSet("PREF", "INTERNET"))
                 .addExpectedNodeWithOrder("EMAIL", "cell@example.com", new TypeSet("CELL"))
-                .addExpectedNodeWithOrder("NOTE", "The following note is the example from RFC 2045.")
+                .addExpectedNodeWithOrder("NOTE",
+                        "The following note is the example from RFC 2045.")
                 .addExpectedNodeWithOrder("NOTE",
                         "Now's the time for all folk to come to the aid of their country.",
                         null, null, mContentValuesForQP, null, null)
@@ -677,12 +681,24 @@
                 .put(Website.TYPE, Website.TYPE_HOMEPAGE);
     }
 
+    public void testInvalidMultipleLineV21() {
+        mVerifier.initForImportTest(V21, R.raw.v21_invalid_multiple_line);
+        ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem();
+        elem.addExpected(StructuredName.CONTENT_ITEM_TYPE)
+                .put(StructuredName.GIVEN_NAME, "Omega")
+                .put(StructuredName.DISPLAY_NAME, "Omega");
+        elem.addExpected(Email.CONTENT_ITEM_TYPE)
+                .put(Email.TYPE, Email.TYPE_CUSTOM)
+                .put(Email.LABEL, "INTERNET")
+                .put(Email.ADDRESS, "\"Omega\" <omega@example.com>");
+    }
+
     public void testV30Simple_Parsing() {
         mVerifier.initForImportTest(V30, R.raw.v30_simple);
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "3.0")
                 .addExpectedNodeWithOrder("FN", "And Roid")
-                .addExpectedNodeWithOrder("N", "And;Roid;;;", Arrays.asList("And", "Roid", "", "", ""))
+                .addExpectedNodeWithOrder("N", "And;Roid;;;",
+                        Arrays.asList("And", "Roid", "", "", ""))
                 .addExpectedNodeWithOrder("ORG", "Open;Handset; Alliance",
                         Arrays.asList("Open", "Handset", " Alliance"))
                 .addExpectedNodeWithOrder("SORT-STRING", "android")
@@ -717,10 +733,8 @@
         // Though Japanese careers append ";;;;" at the end of the value of "SOUND",
         // vCard 2.1/3.0 specification does not allow multiple values.
         // Do not need to handle it as multiple values.
-        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS,
-                R.raw.v21_japanese_1);
+        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_japanese_1);
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "2.1", null, null, null, null, null)
                 .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9;;;;",
                         Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9", "", "", "", ""),
                         null, mContentValuesForSJis, null, null)
@@ -752,37 +766,35 @@
 
     /**
      * Verifies vCard with Japanese can be parsed correctly with
-     * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_GENERIC_UTF8}.
+     * {@link com.android.vcard.VCardConfig#VCARD_TYPE_V21_GENERIC}.
      */
     public void testV21Japanese1_Type_Generic_Utf8() {
         testV21Japanese1Common(
-                R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, false);
+                R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_GENERIC, false);
     }
 
     /**
      * Verifies vCard with Japanese can be parsed correctly with
-     * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_JAPANESE_SJIS}.
+     * {@link com.android.vcard.VCardConfig#VCARD_TYPE_V21_JAPANESE}.
      */
     public void testV21Japanese1_Type_Japanese_Sjis() {
         testV21Japanese1Common(
-                R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, true);
+                R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE, true);
     }
 
     /**
      * Verifies vCard with Japanese can be parsed correctly with
-     * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_JAPANESE_UTF8}.
+     * {@link com.android.vcard.VCardConfig#VCARD_TYPE_V21_JAPANESE}.
      * since vCard 2.1 specifies the charset of each line if it contains non-Ascii.
      */
     public void testV21Japanese1_Type_Japanese_Utf8() {
         testV21Japanese1Common(
-                R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE_UTF8, true);
+                R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE, true);
     }
 
     public void testV21Japanese2_Parsing() {
-        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS,
-                R.raw.v21_japanese_2);
+        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_japanese_2);
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "2.1")
                 .addExpectedNodeWithOrder("N", "\u5B89\u85E4;\u30ED\u30A4\u30C9\u0031;;;",
                         Arrays.asList("\u5B89\u85E4", "\u30ED\u30A4\u30C9\u0031",
                                 "", "", ""),
@@ -838,10 +850,8 @@
     }
 
     public void testV21MultipleEntryCase_Parse() {
-        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS,
-                R.raw.v21_multiple_entry);
+        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_multiple_entry);
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "2.1")
                 .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033;;;;",
                         Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0033", "", "", "", ""),
                         null, mContentValuesForSJis, null, null)
@@ -854,7 +864,6 @@
                 .addExpectedNodeWithOrder("TEL", "11", new TypeSet("X-NEC-SCHOOL"))
                 .addExpectedNodeWithOrder("TEL", "12", new TypeSet("FAX", "HOME"));
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "2.1")
                 .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034;;;;",
                         Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0034", "", "", "", ""),
                         null, mContentValuesForSJis, null, null)
@@ -867,7 +876,6 @@
                 .addExpectedNodeWithOrder("TEL", "15", new TypeSet("X-NEC-FAMILY"))
                 .addExpectedNodeWithOrder("TEL", "16", new TypeSet("X-NEC-GIRL"));
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "2.1")
                 .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035;;;;",
                         Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0035", "", "", "", ""),
                         null, mContentValuesForSJis, null, null)
@@ -882,8 +890,7 @@
     }
 
     public void testV21MultipleEntryCase() {
-        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS,
-                R.raw.v21_multiple_entry);
+        mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_multiple_entry);
         ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem();
         elem.addExpected(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033")
@@ -957,7 +964,6 @@
         ContentValues contentValuesForValue = new ContentValues();
         contentValuesForValue.put("VALUE", "DATE");
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "2.1")
                 .addExpectedNodeWithOrder("N", Arrays.asList("Example", "", "", "", ""))
                 .addExpectedNodeWithOrder("FN", "Example")
                 .addExpectedNodeWithOrder("ANNIVERSARY", "20091010", contentValuesForValue)
@@ -974,6 +980,9 @@
         elem.addExpected(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.FAMILY_NAME, "Example")
                 .put(StructuredName.DISPLAY_NAME, "Example");
+        elem.addExpected(Event.CONTENT_ITEM_TYPE)
+                .put(Event.TYPE, Event.TYPE_ANNIVERSARY)
+                .put(Event.START_DATE, "20091010");
     }
 
     public void testTolerateInvalidCommentLikeLineV21() {
@@ -988,16 +997,15 @@
     }
 
     public void testPagerV30_Parse() {
-        mVerifier.initForImportTest(V30, R.raw.v30_comma_separated);
+        mVerifier.initForImportTest(V30, R.raw.v30_pager);
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "3.0")
                 .addExpectedNodeWithOrder("N", Arrays.asList("F", "G", "M", "", ""))
                 .addExpectedNodeWithOrder("TEL", "6101231234@pagersample.com",
                         new TypeSet("WORK", "MSG", "PAGER"));
     }
 
     public void testPagerV30() {
-        mVerifier.initForImportTest(V30, R.raw.v30_comma_separated);
+        mVerifier.initForImportTest(V30, R.raw.v30_pager);
         ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem();
         elem.addExpected(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.FAMILY_NAME, "F")
@@ -1012,7 +1020,6 @@
     public void testMultiBytePropV30_Parse() {
         mVerifier.initForImportTest(V30, R.raw.v30_multibyte_param);
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("VERSION", "3.0")
                 .addExpectedNodeWithOrder("N", Arrays.asList("F", "G", "M", "", ""))
                 .addExpectedNodeWithOrder("TEL", "1", new TypeSet("\u8D39"));
     }
@@ -1030,4 +1037,72 @@
                 .put(Phone.LABEL, "\u8D39")
                 .put(Phone.NUMBER, "1");
     }
+
+    public void testCommaSeparatedParamsV30_Parse() {
+        mVerifier.initForImportTest(V30, R.raw.v30_comma_separated);
+        mVerifier.addPropertyNodesVerifierElem()
+                .addExpectedNodeWithOrder("N", Arrays.asList("F", "G", "M", "", ""),
+                        new TypeSet("PREF", "HOME"))
+                .addExpectedNodeWithOrder("TEL", "1",
+                        new TypeSet("COMMA,SEPARATED:INSIDE.DQUOTE", "PREF"));
+    }
+
+    public void testSortAsV40_Parse() {
+        mVerifier.initForImportTest(V40, R.raw.v40_sort_as);
+
+        final ContentValues contentValuesForSortAsN = new ContentValues();
+        contentValuesForSortAsN.put("SORT-AS",
+                "\u3042\u3093\u3069\u3046;\u308D\u3044\u3069");
+        final ContentValues contentValuesForSortAsOrg = new ContentValues();
+        contentValuesForSortAsOrg.put("SORT-AS",
+                "\u3050\u30FC\u3050\u308B;\u3051\u3093\u3055\u304F\u3076\u3082\u3093");
+
+        mVerifier.addPropertyNodesVerifierElem()
+                .addExpectedNodeWithOrder("FN", "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9")
+                .addExpectedNodeWithOrder("N",
+                        Arrays.asList("\u5B89\u85E4", "\u30ED\u30A4\u30C9", "", "", ""),
+                        contentValuesForSortAsN)
+                .addExpectedNodeWithOrder("ORG",
+                        Arrays.asList("\u30B0\u30FC\u30B0\u30EB", "\u691C\u7D22\u90E8\u9580"),
+                        contentValuesForSortAsOrg, new TypeSet("WORK"));
+    }
+
+    public void testSortAsV40() {
+        mVerifier.initForImportTest(V40, R.raw.v40_sort_as);
+        final ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem();
+        elem.addExpected(StructuredName.CONTENT_ITEM_TYPE)
+                .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4")
+                .put(StructuredName.GIVEN_NAME, "\u30ED\u30A4\u30C9")
+                .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9")
+                .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3042\u3093\u3069\u3046")
+                .put(StructuredName.PHONETIC_GIVEN_NAME,
+                        "\u308D\u3044\u3069");
+        elem.addExpected(Organization.CONTENT_ITEM_TYPE)
+                .put(Organization.TYPE, Organization.TYPE_WORK)
+                .put(Organization.COMPANY, "\u30B0\u30FC\u30B0\u30EB")
+                .put(Organization.DEPARTMENT, "\u691C\u7D22\u90E8\u9580")
+                .put(Organization.PHONETIC_NAME,
+                        "\u3050\u30FC\u3050\u308B\u3051\u3093\u3055\u304F\u3076\u3082\u3093");
+    }
+
+    public void testIMV21_Parse() {
+        mVerifier.initForImportTest(V21, R.raw.v21_im);
+        mVerifier.addPropertyNodesVerifierElem()
+                .addExpectedNodeWithOrder("X-ANDROID-CUSTOM",
+                        Arrays.asList("vnd.android.cursor.item/nickname", "Nick", "1",
+                                "", "", "", "", "", "", "", "", "", "", "", "", ""))  // 13
+                .addExpectedNodeWithOrder("X-GOOGLE-TALK", "hhh@gmail.com");
+    }
+
+    public void testIMV21() {
+        mVerifier.initForImportTest(V21, R.raw.v21_im);
+        final ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem();
+        elem.addExpected(Nickname.CONTENT_ITEM_TYPE)
+                .put(Nickname.NAME, "Nick")
+                .put(Nickname.TYPE, "1");
+        elem.addExpected(Im.CONTENT_ITEM_TYPE)
+                .put(Im.PROTOCOL, Im.PROTOCOL_GOOGLE_TALK)
+                .put(Im.TYPE, Im.TYPE_HOME)
+                .put(Im.DATA, "hhh@gmail.com");
+    }
 }
diff --git a/core/tests/coretests/src/android/pim/vcard/VCardJapanizationTests.java b/core/tests/coretests/src/android/pim/vcard/VCardJapanizationTests.java
index 5b60342..8a99419 100644
--- a/core/tests/coretests/src/android/pim/vcard/VCardJapanizationTests.java
+++ b/core/tests/coretests/src/android/pim/vcard/VCardJapanizationTests.java
@@ -13,19 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package android.pim.vcard;
 
 import android.content.ContentValues;
-import android.pim.vcard.VCardConfig;
+import android.pim.vcard.test_utils.ContactEntry;
+import android.pim.vcard.test_utils.ContentValuesBuilder;
+import android.pim.vcard.test_utils.PropertyNodesVerifierElem;
+import android.pim.vcard.test_utils.PropertyNodesVerifierElem.TypeSet;
+import android.pim.vcard.test_utils.VCardTestsBase;
 import android.provider.ContactsContract.CommonDataKinds.Nickname;
 import android.provider.ContactsContract.CommonDataKinds.Note;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 
-import android.pim.vcard.PropertyNodesVerifierElem.TypeSet;
-
 import java.util.Arrays;
 
 public class VCardJapanizationTests extends VCardTestsBase {
@@ -39,7 +40,7 @@
                 .put(StructuredName.PREFIX, "Dr.")
                 .put(StructuredName.SUFFIX, "Ph.D");
         ContentValues contentValues =
-            (VCardConfig.isV30(vcardType) ? null : mContentValuesForQPAndUtf8);
+            (VCardConfig.isVersion21(vcardType) ? mContentValuesForQPAndUtf8 : null);
         mVerifier.addPropertyNodesVerifierElem()
                 .addExpectedNode("FN", "Dr. \u3075\u308B\u3069 B \u3091\u308A\u304B Ph.D",
                         contentValues)
@@ -50,15 +51,15 @@
     }
 
     public void testNameUtf8V21() {
-        testNameUtf8Common(VCardConfig.VCARD_TYPE_V21_JAPANESE_UTF8);
+        testNameUtf8Common(VCardConfig.VCARD_TYPE_V21_JAPANESE);
     }
 
     public void testNameUtf8V30() {
-        testNameUtf8Common(VCardConfig.VCARD_TYPE_V30_JAPANESE_UTF8);
+        testNameUtf8Common(VCardConfig.VCARD_TYPE_V30_JAPANESE);
     }
 
     public void testNameShiftJis() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V30_JAPANESE_SJIS);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V30_JAPANESE, "Shift_JIS");
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069")
@@ -80,7 +81,7 @@
      * DoCoMo phones require all name elements should be in "family name" field.
      */
     public void testNameDoCoMo() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS");
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069")
@@ -105,8 +106,8 @@
                 .addExpectedNode("X-DCM-HMN-MODE", "");
     }
 
-    private void testPhoneticNameCommon(int vcardType) {
-        mVerifier.initForExportTest(vcardType);
+    private void testPhoneticNameCommon(int vcardType, String charset) {
+        mVerifier.initForExportTest(vcardType, charset);
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060")
@@ -114,10 +115,10 @@
                 .put(StructuredName.PHONETIC_GIVEN_NAME, "\u305F\u308D\u3046");
 
         final ContentValues contentValues =
-            (VCardConfig.usesShiftJis(vcardType) ?
-                    (VCardConfig.isV30(vcardType) ? mContentValuesForSJis :
-                            mContentValuesForQPAndSJis) :
-                    (VCardConfig.isV30(vcardType) ? null : mContentValuesForQPAndUtf8));
+            ("SHIFT_JIS".equalsIgnoreCase(charset) ?
+                    (VCardConfig.isVersion21(vcardType) ? mContentValuesForQPAndSJis :
+                        mContentValuesForSJis) :
+                    (VCardConfig.isVersion21(vcardType) ? mContentValuesForQPAndUtf8 : null));
         PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElemWithEmptyName();
         elem.addExpectedNode("X-PHONETIC-LAST-NAME", "\u3084\u307E\u3060",
                         contentValues)
@@ -126,7 +127,7 @@
                         contentValues)
                 .addExpectedNode("X-PHONETIC-FIRST-NAME", "\u305F\u308D\u3046",
                         contentValues);
-        if (VCardConfig.isV30(vcardType)) {
+        if (!VCardConfig.isVersion21(vcardType)) {
             elem.addExpectedNode("SORT-STRING",
                     "\u3084\u307E\u3060 \u30DF\u30C9\u30EB\u30CD\u30FC\u30E0 \u305F\u308D\u3046",
                     contentValues);
@@ -142,23 +143,23 @@
     }
 
     public void testPhoneticNameForJapaneseV21Utf8() {
-        testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE_UTF8);
+        testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE, null);
     }
 
     public void testPhoneticNameForJapaneseV21Sjis() {
-        testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS);
+        testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE, "Shift_JIS");
     }
 
     public void testPhoneticNameForJapaneseV30Utf8() {
-        testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE_UTF8);
+        testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE, null);
     }
 
     public void testPhoneticNameForJapaneseV30SJis() {
-        testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE_SJIS);
+        testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE, "Shift_JIS");
     }
 
     public void testPhoneticNameForMobileV21_1() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE, "Shift_JIS");
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060")
@@ -182,7 +183,7 @@
     }
 
     public void testPhoneticNameForMobileV21_2() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE, "Shift_JIS");
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE)
                 .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060")
@@ -198,8 +199,8 @@
                 .put(StructuredName.DISPLAY_NAME, "\uFF94\uFF8F\uFF80\uFF9E \uFF80\uFF9B\uFF73");
     }
 
-    private void testPostalAddressWithJapaneseCommon(int vcardType) {
-        mVerifier.initForExportTest(vcardType);
+    private void testPostalAddressWithJapaneseCommon(int vcardType, String charset) {
+        mVerifier.initForExportTest(vcardType, charset);
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE)
                 .put(StructuredPostal.POBOX, "\u79C1\u66F8\u7BB107")
@@ -214,11 +215,11 @@
                 .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM)
                 .put(StructuredPostal.LABEL, "\u304A\u3082\u3061\u304B\u3048\u308A");
 
-        ContentValues contentValues = (VCardConfig.usesShiftJis(vcardType) ?
-                (VCardConfig.isV30(vcardType) ? mContentValuesForSJis :
-                    mContentValuesForQPAndSJis) :
-                (VCardConfig.isV30(vcardType) ? mContentValuesForUtf8 :
-                    mContentValuesForQPAndUtf8));
+        ContentValues contentValues = ("UTF-8".equalsIgnoreCase(charset) ?
+                (VCardConfig.isVersion21(vcardType) ? mContentValuesForQPAndSJis :
+                    mContentValuesForSJis) :
+                (VCardConfig.isVersion21(vcardType) ? mContentValuesForQPAndUtf8 :
+                    mContentValuesForUtf8));
 
         PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElemWithEmptyName();
         // LABEL must be ignored in vCard 2.1. As for vCard 3.0, the current behavior is
@@ -240,7 +241,7 @@
                 .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME);
     }
     public void testPostalAddresswithJapaneseV21() {
-        testPostalAddressWithJapaneseCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS);
+        testPostalAddressWithJapaneseCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE, "Shift_JIS");
     }
 
     /**
@@ -248,7 +249,7 @@
      * Prefered type must (should?) be: HOME > WORK > OTHER > CUSTOM
      */
     public void testPostalAdrressForDoCoMo_1() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS");
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE)
                 .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK)
@@ -276,7 +277,7 @@
     }
 
     public void testPostalAdrressForDoCoMo_2() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS");
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE)
                 .put(StructuredPostal.TYPE, StructuredPostal.TYPE_OTHER)
@@ -301,7 +302,7 @@
     }
 
     public void testPostalAdrressForDoCoMo_3() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS");
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE)
                 .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM)
@@ -329,7 +330,7 @@
      * Verifies the vCard exporter tolerates null TYPE.
      */
     public void testPostalAdrressForDoCoMo_4() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS");
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE)
                 .put(StructuredPostal.POBOX, "1");
@@ -371,15 +372,15 @@
     }
 
     public void testJapanesePhoneNumberV21_1() {
-        testJapanesePhoneNumberCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE_UTF8);
+        testJapanesePhoneNumberCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE);
     }
 
     public void testJapanesePhoneNumberV30() {
-        testJapanesePhoneNumberCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE_UTF8);
+        testJapanesePhoneNumberCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE);
     }
 
     public void testJapanesePhoneNumberDoCoMo() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS");
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(Phone.CONTENT_ITEM_TYPE)
                 .put(Phone.NUMBER, "0312341234")
@@ -399,7 +400,7 @@
     }
 
     public void testNoteDoCoMo() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS");
         ContactEntry entry = mVerifier.addInputEntry();
         entry.addContentValues(Note.CONTENT_ITEM_TYPE)
                 .put(Note.NOTE, "note1");
@@ -421,7 +422,7 @@
     }
 
     public void testAndroidCustomV21() {
-        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8);
+        mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_GENERIC);
         mVerifier.addInputEntry().addContentValues(Nickname.CONTENT_ITEM_TYPE)
                 .put(Nickname.NAME, "\u304D\u3083\u30FC\u30A8\u30C3\u30C1\u30FC");
         mVerifier.addPropertyNodesVerifierElemWithEmptyName()
diff --git a/core/tests/coretests/src/android/pim/vcard/VCardTestRunner.java b/core/tests/coretests/src/android/pim/vcard/VCardTestRunner.java
new file mode 100644
index 0000000..a3a4393
--- /dev/null
+++ b/core/tests/coretests/src/android/pim/vcard/VCardTestRunner.java
@@ -0,0 +1,42 @@
+/*
+ * 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.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+
+import junit.framework.TestSuite;
+
+/**
+ * Usage: adb shell am instrument -w com.android.vcard.tests/.VCardTestRunnerTestRunner
+ */
+public class VCardTestRunner extends InstrumentationTestRunner {
+    @Override
+    public TestSuite getAllTests() {
+        TestSuite suite = new InstrumentationTestSuite(this);
+        suite.addTestSuite(VCardUtilsTests.class);
+        suite.addTestSuite(VCardTestUtilsTests.class);
+        suite.addTestSuite(VCardImporterTests.class);
+        suite.addTestSuite(VCardExporterTests.class);
+        suite.addTestSuite(VCardJapanizationTests.class);
+        return suite;
+    }
+
+    @Override
+    public ClassLoader getLoader() {
+        return VCardTestRunner.class.getClassLoader();
+    }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/pim/vcard/VCardTestUtilsTests.java b/core/tests/coretests/src/android/pim/vcard/VCardTestUtilsTests.java
new file mode 100644
index 0000000..3ff5cf7
--- /dev/null
+++ b/core/tests/coretests/src/android/pim/vcard/VCardTestUtilsTests.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.android.frameworks.coretests.R;
+
+import android.pim.vcard.test_utils.VCardVerifier;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.AndroidTestCase;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+
+/**
+ * Tests confirming utilities for vCard tests work fine.
+ *
+ * Now that the foundation classes for vCard test cases became too complicated to
+ * rely on without testing itself.
+ */
+public class VCardTestUtilsTests extends AndroidTestCase {
+    public void testShouldFailAtPropertyNodeVerification() {
+        boolean failureDetected = false;
+        try {
+            final VCardVerifier verifier = new VCardVerifier(this);
+            verifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_GENERIC, R.raw.v21_backslash);
+            verifier.addPropertyNodesVerifierElem()
+                    .addExpectedNodeWithOrder("N", ";A;B\\;C\\;;D;:E;\\\\;--",  // wrong
+                            Arrays.asList("", "A;B\\", "C\\;", "D", ":E", "\\\\", ""))
+                    .addExpectedNodeWithOrder("FN", "A;B\\C\\;D:E\\\\");
+            verifier.verify();
+        } catch (AssertionFailedError e) {
+            failureDetected = true;
+        }
+        if (!failureDetected) {
+            TestCase.fail("Test case that should fail actually succeeded.");
+        }
+    }
+
+    public void testShouldFailAtContentValueVerification() {
+        boolean failureDetected = false;
+        try {
+            final VCardVerifier verifier = new VCardVerifier(this);
+            verifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_GENERIC, R.raw.v21_backslash);
+            verifier.addContentValuesVerifierElem()
+                    .addExpected(StructuredName.CONTENT_ITEM_TYPE)
+                            .put(StructuredName.GIVEN_NAME, "A;B\\")
+                            .put(StructuredName.MIDDLE_NAME, "C\\;")
+                            .put(StructuredName.PREFIX, "D")
+                            .put(StructuredName.SUFFIX, ":E");
+            // DISPLAY_NAME is missing.
+            verifier.verify();
+        } catch (AssertionFailedError e) {
+            failureDetected = true;
+        }
+        if (!failureDetected) {
+            TestCase.fail("Test case that should fail actually succeeded.");
+        }
+    }
+
+    public void testShouldFailAtLineVerification() {
+        boolean failureDetected = false;
+        try {
+            final VCardVerifier verifier = new VCardVerifier(this);
+            verifier.initForExportTest(VCardConfig.VCARD_TYPE_V30_GENERIC);
+            verifier.addInputEntry().addContentValues(StructuredName.CONTENT_ITEM_TYPE)
+                    .put(StructuredName.FAMILY_NAME, "\\")
+                    .put(StructuredName.GIVEN_NAME, ";")
+                    .put(StructuredName.MIDDLE_NAME, ",")
+                    .put(StructuredName.PREFIX, "\n")
+                    .put(StructuredName.DISPLAY_NAME, "[<{Unescaped:Asciis}>]");
+            verifier.addLineVerifierElem()
+                    .addExpected("TEL:1")  // wrong
+                    .addExpected("FN:[<{Unescaped:Asciis}>]");
+            verifier.verify();
+        } catch (AssertionFailedError e) {
+            failureDetected = true;
+        }
+        if (!failureDetected) {
+            TestCase.fail("Test case that should fail actually succeeded.");
+        }
+    }
+
+}
diff --git a/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java b/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java
index e805bee..251fab2 100644
--- a/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java
+++ b/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java
@@ -13,10 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package android.pim.vcard;
 
-import android.pim.vcard.VCardUtils;
+import android.text.TextUtils;
 
 import junit.framework.TestCase;
 
@@ -85,31 +84,35 @@
 
     public void testToStringAvailableAsV30ParamValue() {
         // Smoke tests.
-        assertEquals("HOME", VCardUtils.toStringAvailableAsV30ParameValue("HOME"));
-        assertEquals("TEL", VCardUtils.toStringAvailableAsV30ParameValue("TEL"));
-        assertEquals("PAGER", VCardUtils.toStringAvailableAsV30ParameValue("PAGER"));
+        assertEquals("HOME", VCardUtils.toStringAsV30ParamValue("HOME"));
+        assertEquals("TEL", VCardUtils.toStringAsV30ParamValue("TEL"));
+        assertEquals("PAGER", VCardUtils.toStringAsV30ParamValue("PAGER"));
 
-        assertEquals("\"\"", VCardUtils.toStringAvailableAsV30ParameValue(""));
+        assertTrue(TextUtils.isEmpty(VCardUtils.toStringAsV30ParamValue("")));
+        assertTrue(TextUtils.isEmpty(VCardUtils.toStringAsV30ParamValue(null)));
+        assertTrue(TextUtils.isEmpty(VCardUtils.toStringAsV30ParamValue(" \t")));
 
         // non-Ascii must be allowed
         assertEquals("\u4E8B\u52D9\u6240",
-                VCardUtils.toStringAvailableAsV30ParameValue("\u4E8B\u52D9\u6240"));
+                VCardUtils.toStringAsV30ParamValue("\u4E8B\u52D9\u6240"));
         // Reported as bug report.
-        assertEquals("\u8D39", VCardUtils.toStringAvailableAsV30ParameValue("\u8D39"));
+        assertEquals("\u8D39", VCardUtils.toStringAsV30ParamValue("\u8D39"));
         assertEquals("\"comma,separated\"",
-                VCardUtils.toStringAvailableAsV30ParameValue("comma,separated"));
+                VCardUtils.toStringAsV30ParamValue("comma,separated"));
         assertEquals("\"colon:aware\"",
-                VCardUtils.toStringAvailableAsV30ParameValue("colon:aware"));
+                VCardUtils.toStringAsV30ParamValue("colon:aware"));
         // CTL characters.
         assertEquals("CTLExample",
-                VCardUtils.toStringAvailableAsV30ParameValue("CTL\u0001Example"));
+                VCardUtils.toStringAsV30ParamValue("CTL\u0001Example"));
+        assertTrue(TextUtils.isEmpty(
+                VCardUtils.toStringAsV30ParamValue("\u0001\u0002\u0003")));
         // DQUOTE must be removed.
         assertEquals("quoted",
-                VCardUtils.toStringAvailableAsV30ParameValue("\"quoted\""));
+                VCardUtils.toStringAsV30ParamValue("\"quoted\""));
         // DQUOTE must be removed basically, but we should detect a space, which
         // require us to use DQUOTE again.
         // Right-side has one more illegal dquote to test quote-handle code thoroughly.
         assertEquals("\"Already quoted\"",
-                VCardUtils.toStringAvailableAsV30ParameValue("\"Already quoted\"\""));
+                VCardUtils.toStringAsV30ParamValue("\"Already quoted\"\""));
     }
 }
diff --git a/core/tests/coretests/src/android/pim/vcard/VCardVerifier.java b/core/tests/coretests/src/android/pim/vcard/VCardVerifier.java
deleted file mode 100644
index 3cb5b9b..0000000
--- a/core/tests/coretests/src/android/pim/vcard/VCardVerifier.java
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- * Copyright (C) 2009 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.content.ContentProvider;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.EntityIterator;
-import android.net.Uri;
-import android.pim.vcard.VCardComposer;
-import android.pim.vcard.VCardConfig;
-import android.pim.vcard.VCardEntryConstructor;
-import android.pim.vcard.VCardInterpreter;
-import android.pim.vcard.VCardInterpreterCollection;
-import android.pim.vcard.VCardParser;
-import android.pim.vcard.VCardParser_V21;
-import android.pim.vcard.VCardParser_V30;
-import android.pim.vcard.exception.VCardException;
-import android.test.AndroidTestCase;
-import android.test.mock.MockContext;
-import android.util.Log;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.Method;
-import java.util.Arrays;
-
-/* package */ class CustomMockContext extends MockContext {
-    final ContentResolver mResolver;
-    public CustomMockContext(ContentResolver resolver) {
-        mResolver = resolver;
-    }
-
-    @Override
-    public ContentResolver getContentResolver() {
-        return mResolver;
-    }
-}
-
-/* package */ class VCardVerifier {
-    private static final String LOG_TAG = "VCardVerifier";
-
-    private class VCardVerifierInternal implements VCardComposer.OneEntryHandler {
-        public boolean onInit(Context context) {
-            return true;
-        }
-        public boolean onEntryCreated(String vcard) {
-            verifyOneVCard(vcard);
-            return true;
-        }
-        public void onTerminate() {
-        }
-    }
-
-    private final AndroidTestCase mTestCase;
-    private final VCardVerifierInternal mVCardVerifierInternal;
-    private int mVCardType;
-    private boolean mIsV30;
-    private boolean mIsDoCoMo;
-
-    // Only one of them must be non-empty.
-    private ExportTestResolver mExportTestResolver;
-    private InputStream mInputStream;
-
-    // To allow duplication, use list instead of set.
-    // When null, we don't need to do the verification.
-    private PropertyNodesVerifier mPropertyNodesVerifier;
-    private LineVerifier mLineVerifier;
-    private ContentValuesVerifier mContentValuesVerifier;
-    private boolean mInitialized;
-    private boolean mVerified = false;
-
-    public VCardVerifier(AndroidTestCase androidTestCase) {
-        mTestCase = androidTestCase;
-        mVCardVerifierInternal = new VCardVerifierInternal();
-        mExportTestResolver = null;
-        mInputStream = null;
-        mInitialized = false;
-        mVerified = false;
-    }
-
-    public void initForExportTest(int vcardType) {
-        if (mInitialized) {
-            mTestCase.fail("Already initialized");
-        }
-        mExportTestResolver = new ExportTestResolver(mTestCase);
-        mVCardType = vcardType;
-        mIsV30 = VCardConfig.isV30(vcardType);
-        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
-        mInitialized = true;
-    }
-
-    public void initForImportTest(int vcardType, int resId) {
-        if (mInitialized) {
-            mTestCase.fail("Already initialized");
-        }
-        mVCardType = vcardType;
-        mIsV30 = VCardConfig.isV30(vcardType);
-        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
-        setInputResourceId(resId);
-        mInitialized = true;
-    }
-
-    private void setInputResourceId(int resId) {
-        InputStream inputStream = mTestCase.getContext().getResources().openRawResource(resId);
-        if (inputStream == null) {
-            mTestCase.fail("Wrong resId: " + resId);
-        }
-        setInputStream(inputStream);
-    }
-
-    private void setInputStream(InputStream inputStream) {
-        if (mExportTestResolver != null) {
-            mTestCase.fail("addInputEntry() is called.");
-        } else if (mInputStream != null) {
-            mTestCase.fail("InputStream is already set");
-        }
-        mInputStream = inputStream;
-    }
-
-    public ContactEntry addInputEntry() {
-        if (!mInitialized) {
-            mTestCase.fail("Not initialized");
-        }
-        if (mInputStream != null) {
-            mTestCase.fail("setInputStream is called");
-        }
-        return mExportTestResolver.addInputContactEntry();
-    }
-
-    public PropertyNodesVerifierElem addPropertyNodesVerifierElem() {
-        if (!mInitialized) {
-            mTestCase.fail("Not initialized");
-        }
-        if (mPropertyNodesVerifier == null) {
-            mPropertyNodesVerifier = new PropertyNodesVerifier(mTestCase);
-        }
-        PropertyNodesVerifierElem elem =
-                mPropertyNodesVerifier.addPropertyNodesVerifierElem();
-        elem.addExpectedNodeWithOrder("VERSION", (mIsV30 ? "3.0" : "2.1"));
-
-        return elem;
-    }
-
-    public PropertyNodesVerifierElem addPropertyNodesVerifierElemWithEmptyName() {
-        if (!mInitialized) {
-            mTestCase.fail("Not initialized");
-        }
-        PropertyNodesVerifierElem elem = addPropertyNodesVerifierElem();
-        if (mIsV30) {
-            elem.addExpectedNodeWithOrder("N", "").addExpectedNodeWithOrder("FN", "");
-        } else if (mIsDoCoMo) {
-            elem.addExpectedNodeWithOrder("N", "");
-        }
-        return elem;
-    }
-
-    public LineVerifierElem addLineVerifierElem() {
-        if (!mInitialized) {
-            mTestCase.fail("Not initialized");
-        }
-        if (mLineVerifier == null) {
-            mLineVerifier = new LineVerifier(mTestCase, mVCardType);
-        }
-        return mLineVerifier.addLineVerifierElem();
-    }
-
-    public ContentValuesVerifierElem addContentValuesVerifierElem() {
-        if (!mInitialized) {
-            mTestCase.fail("Not initialized");
-        }
-        if (mContentValuesVerifier == null) {
-            mContentValuesVerifier = new ContentValuesVerifier();
-        }
-
-        return mContentValuesVerifier.addElem(mTestCase);
-    }
-
-    private void verifyOneVCard(final String vcard) {
-        // Log.d("@@@", vcard);
-        final VCardInterpreter builder;
-        if (mContentValuesVerifier != null) {
-            final VNodeBuilder vnodeBuilder = mPropertyNodesVerifier;
-            final VCardEntryConstructor vcardDataBuilder =
-                    new VCardEntryConstructor(mVCardType);
-            vcardDataBuilder.addEntryHandler(mContentValuesVerifier);
-            if (mPropertyNodesVerifier != null) {
-                builder = new VCardInterpreterCollection(Arrays.asList(
-                        mPropertyNodesVerifier, vcardDataBuilder));
-            } else {
-                builder = vnodeBuilder;
-            }
-        } else {
-            if (mPropertyNodesVerifier != null) {
-                builder = mPropertyNodesVerifier;
-            } else {
-                return;
-            }
-        }
-
-        final VCardParser parser =
-                (mIsV30 ? new VCardParser_V30(true) : new VCardParser_V21());
-        InputStream is = null;
-        try {
-            String charset =
-                (VCardConfig.usesShiftJis(mVCardType) ? "SHIFT_JIS" : "UTF-8");
-            is = new ByteArrayInputStream(vcard.getBytes(charset));
-            mTestCase.assertEquals(true, parser.parse(is, null, builder));
-        } catch (IOException e) {
-            mTestCase.fail("Unexpected IOException: " + e.getMessage());
-        } catch (VCardException e) {
-            mTestCase.fail("Unexpected VCardException: " + e.getMessage());
-        } finally {
-            if (is != null) {
-                try {
-                    is.close();
-                } catch (IOException e) {
-                }
-            }
-        }
-    }
-
-    public void verify() {
-        if (!mInitialized) {
-            mTestCase.fail("Not initialized.");
-        }
-        if (mVerified) {
-            mTestCase.fail("verify() was called twice.");
-        }
-        if (mInputStream != null) {
-            try {
-                verifyForImportTest();
-            } catch (IOException e) {
-                mTestCase.fail("IOException was thrown: " + e.getMessage());
-            } catch (VCardException e) {
-                mTestCase.fail("VCardException was thrown: " + e.getMessage());
-            }
-        } else if (mExportTestResolver != null){
-            verifyForExportTest();
-        } else {
-            mTestCase.fail("No input is determined");
-        }
-        mVerified = true;
-    }
-
-    private void verifyForImportTest() throws IOException, VCardException {
-        if (mLineVerifier != null) {
-            mTestCase.fail("Not supported now.");
-        }
-        if (mContentValuesVerifier != null) {
-            mContentValuesVerifier.verify(mInputStream, mVCardType);
-        }
-    }
-
-    public static EntityIterator mockGetEntityIteratorMethod(
-            final ContentResolver resolver,
-            final Uri uri, final String selection,
-            final String[] selectionArgs, final String sortOrder) {
-        if (ExportTestResolver.class.equals(resolver.getClass())) {
-            return ((ExportTestResolver)resolver).getProvider().queryEntities(
-                    uri, selection, selectionArgs, sortOrder);
-        }
-
-        Log.e(LOG_TAG, "Unexpected provider given.");
-        return null;
-    }
-
-    private Method getMockGetEntityIteratorMethod()
-            throws SecurityException, NoSuchMethodException {
-        return this.getClass().getMethod("mockGetEntityIteratorMethod",
-                ContentResolver.class, Uri.class, String.class, String[].class, String.class);
-    }
-
-    private void verifyForExportTest() {
-       final VCardComposer composer =
-            new VCardComposer(new CustomMockContext(mExportTestResolver), mVCardType);
-        composer.addHandler(mLineVerifier);
-        composer.addHandler(mVCardVerifierInternal);
-        if (!composer.init(VCardComposer.CONTACTS_TEST_CONTENT_URI, null, null, null)) {
-            AndroidTestCase.fail("init() failed. Reason: " + composer.getErrorReason());
-        }
-        AndroidTestCase.assertFalse(composer.isAfterLast());
-        try {
-            while (!composer.isAfterLast()) {
-                try {
-                    final Method mockGetEntityIteratorMethod = getMockGetEntityIteratorMethod();
-                    AndroidTestCase.assertNotNull(mockGetEntityIteratorMethod);
-                    AndroidTestCase.assertTrue(
-                            composer.createOneEntry(mockGetEntityIteratorMethod));
-                } catch (Exception e) {
-                    e.printStackTrace();
-                    AndroidTestCase.fail();
-                }
-            }
-        } finally {
-            composer.terminate();
-        }
-    }
-}
diff --git a/core/tests/coretests/src/android/pim/vcard/test_utils/ContactEntry.java b/core/tests/coretests/src/android/pim/vcard/test_utils/ContactEntry.java
new file mode 100644
index 0000000..843750e
--- /dev/null
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/ContactEntry.java
@@ -0,0 +1,43 @@
+/*
+ * 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.test_utils;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <p>
+ * The class representing one contact, which should contain multiple ContentValues like
+ * StructuredName, Email, etc.
+ * </p>
+ */
+public final class ContactEntry {
+    private final List<ContentValues> mContentValuesList = new ArrayList<ContentValues>();
+
+    public ContentValuesBuilder addContentValues(String mimeType) {
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(Data.MIMETYPE, mimeType);
+        mContentValuesList.add(contentValues);
+        return new ContentValuesBuilder(contentValues);
+    }
+
+    public List<ContentValues> getList() {
+        return mContentValuesList;
+    }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/pim/vcard/ContentValuesBuilder.java b/core/tests/coretests/src/android/pim/vcard/test_utils/ContentValuesBuilder.java
similarity index 95%
rename from core/tests/coretests/src/android/pim/vcard/ContentValuesBuilder.java
rename to core/tests/coretests/src/android/pim/vcard/test_utils/ContentValuesBuilder.java
index b3c0773..bdcccf9 100644
--- a/core/tests/coretests/src/android/pim/vcard/ContentValuesBuilder.java
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/ContentValuesBuilder.java
@@ -13,8 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package android.pim.vcard;
+package android.pim.vcard.test_utils;
 
 import android.content.ContentValues;
 
@@ -22,7 +21,7 @@
  * ContentValues-like class which enables users to chain put() methods and restricts
  * the other methods.
  */
-/* package */ class ContentValuesBuilder {
+public class ContentValuesBuilder {
     private final ContentValues mContentValues;
 
     public ContentValuesBuilder(final ContentValues contentValues) {
@@ -34,6 +33,7 @@
         return this;
     }
 
+    /*
     public ContentValuesBuilder put(String key, Byte value) {
         mContentValues.put(key, value);
         return this;
@@ -42,13 +42,14 @@
     public ContentValuesBuilder put(String key, Short value) {
         mContentValues.put(key, value);
         return this;
-    }
+    }*/
 
     public ContentValuesBuilder put(String key, Integer value) {
         mContentValues.put(key, value);
         return this;
     }
 
+    /*
     public ContentValuesBuilder put(String key, Long value) {
         mContentValues.put(key, value);
         return this;
@@ -67,7 +68,7 @@
     public ContentValuesBuilder put(String key, Boolean value) {
         mContentValues.put(key, value);
         return this;
-    }
+    }*/
 
     public ContentValuesBuilder put(String key, byte[] value) {
         mContentValues.put(key, value);
diff --git a/core/tests/coretests/src/android/pim/vcard/test_utils/ContentValuesVerifier.java b/core/tests/coretests/src/android/pim/vcard/test_utils/ContentValuesVerifier.java
new file mode 100644
index 0000000..d0613c6
--- /dev/null
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/ContentValuesVerifier.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2009 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.test_utils;
+
+import android.pim.vcard.VCardEntry;
+import android.pim.vcard.VCardEntryHandler;
+import android.test.AndroidTestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ContentValuesVerifier implements VCardEntryHandler {
+    private List<ContentValuesVerifierElem> mContentValuesVerifierElemList =
+        new ArrayList<ContentValuesVerifierElem>();
+    private int mIndex;
+
+    public ContentValuesVerifierElem addElem(AndroidTestCase androidTestCase) {
+        ContentValuesVerifierElem elem = new ContentValuesVerifierElem(androidTestCase);
+        mContentValuesVerifierElemList.add(elem);
+        return elem;
+    }
+
+    public void onStart() {
+        for (ContentValuesVerifierElem elem : mContentValuesVerifierElemList) {
+            elem.onParsingStart();
+        }
+    }
+
+    public void onEntryCreated(VCardEntry entry) {
+        AndroidTestCase.assertTrue(mIndex < mContentValuesVerifierElemList.size());
+        mContentValuesVerifierElemList.get(mIndex).onEntryCreated(entry);
+        mIndex++;
+    }
+
+    public void onEnd() {
+        for (ContentValuesVerifierElem elem : mContentValuesVerifierElemList) {
+            elem.onParsingEnd();
+            elem.verifyResolver();
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifierElem.java b/core/tests/coretests/src/android/pim/vcard/test_utils/ContentValuesVerifierElem.java
similarity index 78%
rename from core/tests/coretests/src/android/pim/vcard/ContentValuesVerifierElem.java
rename to core/tests/coretests/src/android/pim/vcard/test_utils/ContentValuesVerifierElem.java
index 2edbb36..03c18e1 100644
--- a/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifierElem.java
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/ContentValuesVerifierElem.java
@@ -13,17 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.pim.vcard;
+package android.pim.vcard.test_utils;
 
 import android.content.ContentValues;
-import android.pim.vcard.VCardConfig;
 import android.pim.vcard.VCardEntry;
 import android.pim.vcard.VCardEntryCommitter;
 import android.pim.vcard.VCardEntryConstructor;
 import android.pim.vcard.VCardEntryHandler;
 import android.pim.vcard.VCardParser;
-import android.pim.vcard.VCardParser_V21;
-import android.pim.vcard.VCardParser_V30;
+import android.pim.vcard.VCardUtils;
 import android.pim.vcard.exception.VCardException;
 import android.provider.ContactsContract.Data;
 import android.test.AndroidTestCase;
@@ -31,7 +29,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 
-/* package */ class ContentValuesVerifierElem {
+public class ContentValuesVerifierElem {
     private final AndroidTestCase mTestCase;
     private final ImportTestResolver mResolver;
     private final VCardEntryHandler mHandler;
@@ -49,20 +47,14 @@
         return new ContentValuesBuilder(contentValues);
     }
 
-    public void verify(int resId, int vCardType)
+    public void verify(int resId, int vcardType)
             throws IOException, VCardException {
-        verify(mTestCase.getContext().getResources().openRawResource(resId), vCardType);
+        verify(mTestCase.getContext().getResources().openRawResource(resId), vcardType);
     }
 
-    public void verify(InputStream is, int vCardType) throws IOException, VCardException {
-        final VCardParser vCardParser;
-        if (VCardConfig.isV30(vCardType)) {
-            vCardParser = new VCardParser_V30(true);  // use StrictParsing
-        } else {
-            vCardParser = new VCardParser_V21();
-        }
-        VCardEntryConstructor builder =
-                new VCardEntryConstructor(null, null, false, vCardType, null);
+    public void verify(InputStream is, int vcardType) throws IOException, VCardException {
+        final VCardParser vCardParser = VCardUtils.getAppropriateParser(vcardType);
+        final VCardEntryConstructor builder = new VCardEntryConstructor(vcardType, null);
         builder.addEntryHandler(mHandler);
         try {
             vCardParser.parse(is, builder);
diff --git a/core/tests/coretests/src/android/pim/vcard/test_utils/ExportTestProvider.java b/core/tests/coretests/src/android/pim/vcard/test_utils/ExportTestProvider.java
new file mode 100644
index 0000000..e095258
--- /dev/null
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/ExportTestProvider.java
@@ -0,0 +1,174 @@
+/*
+ * 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.test_utils;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.database.Cursor;
+import android.net.Uri;
+import android.pim.vcard.VCardComposer;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockCursor;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public class ExportTestProvider extends MockContentProvider {
+    final private ArrayList<ContactEntry> mContactEntryList = new ArrayList<ContactEntry>();
+
+    private static class MockEntityIterator implements EntityIterator {
+        List<Entity> mEntityList;
+        Iterator<Entity> mIterator;
+
+        public MockEntityIterator(List<ContentValues> contentValuesList) {
+            mEntityList = new ArrayList<Entity>();
+            Entity entity = new Entity(new ContentValues());
+            for (ContentValues contentValues : contentValuesList) {
+                    entity.addSubValue(Data.CONTENT_URI, contentValues);
+            }
+            mEntityList.add(entity);
+            mIterator = mEntityList.iterator();
+        }
+
+        public boolean hasNext() {
+            return mIterator.hasNext();
+        }
+
+        public Entity next() {
+            return mIterator.next();
+        }
+
+        public void remove() {
+            throw new UnsupportedOperationException("remove not supported");
+        }
+
+        public void reset() {
+            mIterator = mEntityList.iterator();
+        }
+
+        public void close() {
+        }
+    }
+
+    public ExportTestProvider(AndroidTestCase androidTestCase) {
+    }
+
+    public ContactEntry buildInputEntry() {
+        ContactEntry contactEntry = new ContactEntry();
+        mContactEntryList.add(contactEntry);
+        return contactEntry;
+    }
+
+    /**
+     * <p>
+     * An old method which had existed but was removed from ContentResolver.
+     * </p>
+     * <p>
+     * We still keep using this method since we don't have a propeer way to know
+     * which value in the ContentValue corresponds to the entry in Contacts database.
+     * </p>
+     */
+    public EntityIterator queryEntities(Uri uri,
+            String selection, String[] selectionArgs, String sortOrder) {
+        TestCase.assertTrue(uri != null);
+        TestCase.assertTrue(ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()));
+        final String authority = uri.getAuthority();
+        TestCase.assertTrue(RawContacts.CONTENT_URI.getAuthority().equals(authority));
+        TestCase.assertTrue((Data.CONTACT_ID + "=?").equals(selection));
+        TestCase.assertEquals(1, selectionArgs.length);
+        final int id = Integer.parseInt(selectionArgs[0]);
+        TestCase.assertTrue(id >= 0 && id < mContactEntryList.size());
+
+        return new MockEntityIterator(mContactEntryList.get(id).getList());
+    }
+
+    @Override
+    public Cursor query(Uri uri,String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        TestCase.assertTrue(VCardComposer.CONTACTS_TEST_CONTENT_URI.equals(uri));
+        // In this test, following arguments are not supported.
+        TestCase.assertNull(selection);
+        TestCase.assertNull(selectionArgs);
+        TestCase.assertNull(sortOrder);
+
+        return new MockCursor() {
+            int mCurrentPosition = -1;
+
+            @Override
+            public int getCount() {
+                return mContactEntryList.size();
+            }
+
+            @Override
+            public boolean moveToFirst() {
+                mCurrentPosition = 0;
+                return true;
+            }
+
+            @Override
+            public boolean moveToNext() {
+                if (mCurrentPosition < mContactEntryList.size()) {
+                    mCurrentPosition++;
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+
+            @Override
+            public boolean isBeforeFirst() {
+                return mCurrentPosition < 0;
+            }
+
+            @Override
+            public boolean isAfterLast() {
+                return mCurrentPosition >= mContactEntryList.size();
+            }
+
+            @Override
+            public int getColumnIndex(String columnName) {
+                TestCase.assertEquals(Contacts._ID, columnName);
+                return 0;
+            }
+
+            @Override
+            public int getInt(int columnIndex) {
+                TestCase.assertEquals(0, columnIndex);
+                TestCase.assertTrue(mCurrentPosition >= 0
+                        && mCurrentPosition < mContactEntryList.size());
+                return mCurrentPosition;
+            }
+
+            @Override
+            public String getString(int columnIndex) {
+                return String.valueOf(getInt(columnIndex));
+            }
+
+            @Override
+            public void close() {
+            }
+        };
+    }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/pim/vcard/test_utils/ExportTestResolver.java b/core/tests/coretests/src/android/pim/vcard/test_utils/ExportTestResolver.java
new file mode 100644
index 0000000..606d91a
--- /dev/null
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/ExportTestResolver.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2009 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.test_utils;
+
+import android.pim.vcard.VCardComposer;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
+
+public class ExportTestResolver extends MockContentResolver {
+    private final ExportTestProvider mProvider;
+    public ExportTestResolver(AndroidTestCase androidTestCase) {
+        mProvider = new ExportTestProvider(androidTestCase);
+        addProvider(VCardComposer.VCARD_TEST_AUTHORITY, mProvider);
+        addProvider(RawContacts.CONTENT_URI.getAuthority(), mProvider);
+    }
+
+    public ContactEntry addInputContactEntry() {
+        return mProvider.buildInputEntry();
+    }
+
+    public ExportTestProvider getProvider() {
+        return mProvider;
+    }
+}
diff --git a/core/tests/coretests/src/android/pim/vcard/ImportTestResolver.java b/core/tests/coretests/src/android/pim/vcard/test_utils/ImportTestProvider.java
similarity index 82%
rename from core/tests/coretests/src/android/pim/vcard/ImportTestResolver.java
rename to core/tests/coretests/src/android/pim/vcard/test_utils/ImportTestProvider.java
index 1563da9..df89491 100644
--- a/core/tests/coretests/src/android/pim/vcard/ImportTestResolver.java
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/ImportTestProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 The Android Open Source Project
+ * 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.
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.pim.vcard;
+package android.pim.vcard.test_utils;
 
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
@@ -34,8 +34,8 @@
 import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
 import android.test.mock.MockContentProvider;
-import android.test.mock.MockContentResolver;
 import android.text.TextUtils;
 
 import junit.framework.TestCase;
@@ -51,38 +51,7 @@
 import java.util.SortedMap;
 import java.util.TreeMap;
 
-/* package */ class ImportTestResolver extends MockContentResolver {
-    final ImportTestProvider mProvider;
-
-    public ImportTestResolver(TestCase testCase) {
-        mProvider = new ImportTestProvider(testCase);
-    }
-
-    @Override
-    public ContentProviderResult[] applyBatch(String authority,
-            ArrayList<ContentProviderOperation> operations) {
-        equalsString(authority, RawContacts.CONTENT_URI.toString());
-        return mProvider.applyBatch(operations);
-    }
-
-    public void addExpectedContentValues(ContentValues expectedContentValues) {
-        mProvider.addExpectedContentValues(expectedContentValues);
-    }
-
-    public void verify() {
-        mProvider.verify();
-    }
-
-    private static boolean equalsString(String a, String b) {
-        if (a == null || a.length() == 0) {
-            return b == null || b.length() == 0;
-        } else {
-            return a.equals(b);
-        }
-    }
-}
-
-/* package */ class ImportTestProvider extends MockContentProvider {
+public class ImportTestProvider extends MockContentProvider {
     private static final Set<String> sKnownMimeTypeSet =
         new HashSet<String>(Arrays.asList(StructuredName.CONTENT_ITEM_TYPE,
                 Nickname.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE,
@@ -95,10 +64,7 @@
 
     final Map<String, Collection<ContentValues>> mMimeTypeToExpectedContentValues;
 
-    private final TestCase mTestCase;
-
-    public ImportTestProvider(TestCase testCase) {
-        mTestCase = testCase;
+    public ImportTestProvider(AndroidTestCase androidTestCase) {
         mMimeTypeToExpectedContentValues =
             new HashMap<String, Collection<ContentValues>>();
         for (String acceptanbleMimeType : sKnownMimeTypeSet) {
@@ -113,7 +79,7 @@
     public void addExpectedContentValues(ContentValues expectedContentValues) {
         final String mimeType = expectedContentValues.getAsString(Data.MIMETYPE);
         if (!sKnownMimeTypeSet.contains(mimeType)) {
-            mTestCase.fail(String.format(
+            TestCase.fail(String.format(
                     "Unknow MimeType %s in the test code. Test code should be broken.",
                     mimeType));
         }
@@ -127,7 +93,7 @@
     public ContentProviderResult[] applyBatch(
             ArrayList<ContentProviderOperation> operations) {
         if (operations == null) {
-            mTestCase.fail("There is no operation.");
+            TestCase.fail("There is no operation.");
         }
 
         final int size = operations.size();
@@ -148,12 +114,12 @@
                     fakeResultArray, i);
             final Uri uri = operation.getUri();
             if (uri.equals(RawContacts.CONTENT_URI)) {
-                mTestCase.assertNull(actualContentValues.get(RawContacts.ACCOUNT_NAME));
-                mTestCase.assertNull(actualContentValues.get(RawContacts.ACCOUNT_TYPE));
+                TestCase.assertNull(actualContentValues.get(RawContacts.ACCOUNT_NAME));
+                TestCase.assertNull(actualContentValues.get(RawContacts.ACCOUNT_TYPE));
             } else if (uri.equals(Data.CONTENT_URI)) {
                 final String mimeType = actualContentValues.getAsString(Data.MIMETYPE);
                 if (!sKnownMimeTypeSet.contains(mimeType)) {
-                    mTestCase.fail(String.format(
+                    TestCase.fail(String.format(
                             "Unknown MimeType %s. Probably added after developing this test",
                             mimeType));
                 }
@@ -185,7 +151,7 @@
                 final Collection<ContentValues> contentValuesCollection =
                     mMimeTypeToExpectedContentValues.get(mimeType);
                 if (contentValuesCollection.isEmpty()) {
-                    mTestCase.fail("ContentValues for MimeType " + mimeType
+                    TestCase.fail("ContentValues for MimeType " + mimeType
                             + " is not expected at all (" + actualContentValues + ")");
                 }
                 boolean checked = false;
@@ -197,23 +163,25 @@
                             + convertToEasilyReadableString(actualContentValues));*/
                     if (equalsForContentValues(expectedContentValues,
                             actualContentValues)) {
-                        mTestCase.assertTrue(contentValuesCollection.remove(expectedContentValues));
+                        TestCase.assertTrue(contentValuesCollection.remove(expectedContentValues));
                         checked = true;
                         break;
                     }
                 }
                 if (!checked) {
                     final StringBuilder builder = new StringBuilder();
+                    builder.append("\n");
                     builder.append("Unexpected: ");
                     builder.append(convertToEasilyReadableString(actualContentValues));
-                    builder.append("\nExpected: ");
+                    builder.append("\n");
+                    builder.append("Expected  : ");
                     for (ContentValues expectedContentValues : contentValuesCollection) {
                         builder.append(convertToEasilyReadableString(expectedContentValues));
                     }
-                    mTestCase.fail(builder.toString());
+                    TestCase.fail(builder.toString());
                 }
             } else {
-                mTestCase.fail("Unexpected Uri has come: " + uri);
+                TestCase.fail("Unexpected Uri has come: " + uri);
             }
         }  // for (int i = 0; i < size; i++) {
         return fakeResultArray;
@@ -232,7 +200,7 @@
             final String failMsg =
                 "There is(are) remaining expected ContentValues instance(s): \n"
                     + builder.toString();
-            mTestCase.fail(failMsg);
+            TestCase.fail(failMsg);
         }
     }
 
@@ -252,7 +220,7 @@
             if (Data.MIMETYPE.equals(key)) {
                 mimeTypeValue = valueString;
             } else {
-                mTestCase.assertNotNull(key);
+                TestCase.assertNotNull(key);
                 sortedMap.put(key, valueString);
             }
         }
@@ -273,7 +241,7 @@
     }
 
     private static boolean equalsForContentValues(
-            ContentValues expected, ContentValues actual) {
+            final ContentValues expected, final ContentValues actual) {
         if (expected == actual) {
             return true;
         } else if (expected == null || actual == null || expected.size() != actual.size()) {
@@ -286,15 +254,21 @@
             if (!actual.containsKey(key)) {
                 return false;
             }
+            // Type mismatch usuall happens as importer doesn't care the type of each value.
+            // For example, variable type might be Integer when importing the type of TEL,
+            // while variable type would be String when importing the type of RELATION.
+            final Object actualValue = actual.get(key);
             if (value instanceof byte[]) {
-                Object actualValue = actual.get(key);
                 if (!Arrays.equals((byte[])value, (byte[])actualValue)) {
+                    byte[] e = (byte[])value;
+                    byte[] a = (byte[])actualValue;
                     return false;
                 }
-            } else if (!value.equals(actual.get(key))) {
-                    return false;
+            } else if (!value.equals(actualValue) &&
+                    !value.toString().equals(actualValue.toString())) {
+                return false;
             }
         }
         return true;
     }
-}
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/pim/vcard/test_utils/ImportTestResolver.java b/core/tests/coretests/src/android/pim/vcard/test_utils/ImportTestResolver.java
new file mode 100644
index 0000000..1822afe
--- /dev/null
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/ImportTestResolver.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009 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.test_utils;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentValues;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
+
+import java.util.ArrayList;
+
+public class ImportTestResolver extends MockContentResolver {
+    private final ImportTestProvider mProvider;
+
+    public ImportTestResolver(AndroidTestCase androidTestCase) {
+        mProvider = new ImportTestProvider(androidTestCase);
+    }
+
+    @Override
+    public ContentProviderResult[] applyBatch(String authority,
+            ArrayList<ContentProviderOperation> operations) {
+        equalsString(authority, RawContacts.CONTENT_URI.toString());
+        return mProvider.applyBatch(operations);
+    }
+
+    public void addExpectedContentValues(ContentValues expectedContentValues) {
+        mProvider.addExpectedContentValues(expectedContentValues);
+    }
+
+    public void verify() {
+        mProvider.verify();
+    }
+
+    private static boolean equalsString(String a, String b) {
+        if (a == null || a.length() == 0) {
+            return b == null || b.length() == 0;
+        } else {
+            return a.equals(b);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/pim/vcard/LineVerifier.java b/core/tests/coretests/src/android/pim/vcard/test_utils/LineVerifier.java
similarity index 72%
rename from core/tests/coretests/src/android/pim/vcard/LineVerifier.java
rename to core/tests/coretests/src/android/pim/vcard/test_utils/LineVerifier.java
index cef15fd..e314ac5 100644
--- a/core/tests/coretests/src/android/pim/vcard/LineVerifier.java
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/LineVerifier.java
@@ -13,39 +13,40 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.pim.vcard;
+package android.pim.vcard.test_utils;
 
 import android.content.Context;
 import android.pim.vcard.VCardComposer;
+import android.test.AndroidTestCase;
 
 import junit.framework.TestCase;
 
 import java.util.ArrayList;
 
-class LineVerifier implements VCardComposer.OneEntryHandler {
-    private final TestCase mTestCase;
+public class LineVerifier implements VCardComposer.OneEntryHandler {
+    private final AndroidTestCase mAndroidTestCase;
     private final ArrayList<LineVerifierElem> mLineVerifierElemList;
     private int mVCardType;
     private int index;
 
-    public LineVerifier(TestCase testCase, int vcardType) {
-        mTestCase = testCase;
+    public LineVerifier(AndroidTestCase androidTestCase, int vcardType) {
+        mAndroidTestCase = androidTestCase;
         mLineVerifierElemList = new ArrayList<LineVerifierElem>();
         mVCardType = vcardType;
     }
 
     public LineVerifierElem addLineVerifierElem() {
-        LineVerifierElem lineVerifier = new LineVerifierElem(mTestCase, mVCardType);
+        LineVerifierElem lineVerifier = new LineVerifierElem(mAndroidTestCase, mVCardType);
         mLineVerifierElemList.add(lineVerifier);
         return lineVerifier;
     }
 
     public void verify(String vcard) {
         if (index >= mLineVerifierElemList.size()) {
-            mTestCase.fail("Insufficient number of LineVerifier (" + index + ")");
+            TestCase.fail("Insufficient number of LineVerifier (" + index + ")");
         }
 
-        LineVerifierElem lineVerifier = mLineVerifierElemList.get(index);
+        final LineVerifierElem lineVerifier = mLineVerifierElemList.get(index);
         lineVerifier.verify(vcard);
 
         index++;
diff --git a/core/tests/coretests/src/android/pim/vcard/LineVerifierElem.java b/core/tests/coretests/src/android/pim/vcard/test_utils/LineVerifierElem.java
similarity index 72%
rename from core/tests/coretests/src/android/pim/vcard/LineVerifierElem.java
rename to core/tests/coretests/src/android/pim/vcard/test_utils/LineVerifierElem.java
index b23b29b..e23a9cd 100644
--- a/core/tests/coretests/src/android/pim/vcard/LineVerifierElem.java
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/LineVerifierElem.java
@@ -13,9 +13,10 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package android.pim.vcard;
+package android.pim.vcard.test_utils;
 
 import android.pim.vcard.VCardConfig;
+import android.test.AndroidTestCase;
 import android.text.TextUtils;
 
 import junit.framework.TestCase;
@@ -23,14 +24,12 @@
 import java.util.ArrayList;
 import java.util.List;
 
-class LineVerifierElem {
-    private final TestCase mTestCase;
+public class LineVerifierElem {
     private final List<String> mExpectedLineList = new ArrayList<String>();
-    private final boolean mIsV30;
+    private final int mVCardType;
 
-    public LineVerifierElem(TestCase testCase, int vcardType) {
-        mTestCase = testCase;
-        mIsV30 = VCardConfig.isV30(vcardType);
+    public LineVerifierElem(AndroidTestCase androidTestCase, int vcardType) {
+        mVCardType = vcardType;
     }
 
     public LineVerifierElem addExpected(final String line) {
@@ -55,21 +54,23 @@
 
             if ("BEGIN:VCARD".equalsIgnoreCase(line)) {
                 if (beginExists) {
-                    mTestCase.fail("Multiple \"BEGIN:VCARD\" line found");
+                    TestCase.fail("Multiple \"BEGIN:VCARD\" line found");
                 } else {
                     beginExists = true;
                     continue;
                 }
             } else if ("END:VCARD".equalsIgnoreCase(line)) {
                 if (endExists) {
-                    mTestCase.fail("Multiple \"END:VCARD\" line found");
+                    TestCase.fail("Multiple \"END:VCARD\" line found");
                 } else {
                     endExists = true;
                     continue;
                 }
-            } else if ((mIsV30 ? "VERSION:3.0" : "VERSION:2.1").equalsIgnoreCase(line)) {
+            } else if ((VCardConfig.isVersion21(mVCardType) ? "VERSION:2.1" :
+                (VCardConfig.isVersion30(mVCardType) ? "VERSION:3.0" :
+                    "VERSION:4.0")).equalsIgnoreCase(line)) {
                 if (versionExists) {
-                    mTestCase.fail("Multiple VERSION line + found");
+                    TestCase.fail("Multiple VERSION line + found");
                 } else {
                     versionExists = true;
                     continue;
@@ -77,18 +78,16 @@
             }
 
             if (!beginExists) {
-                mTestCase.fail("Property other than BEGIN came before BEGIN property: "
-                        + line);
+                TestCase.fail("Property other than BEGIN came before BEGIN property: " + line);
             } else if (endExists) {
-                mTestCase.fail("Property other than END came after END property: "
-                        + line);
+                TestCase.fail("Property other than END came after END property: " + line);
             }
 
             final int index = mExpectedLineList.indexOf(line);
             if (index >= 0) {
                 mExpectedLineList.remove(index);
             } else {
-                mTestCase.fail("Unexpected line: " + line);
+                TestCase.fail("Unexpected line: " + line);
             }
         }
 
@@ -99,7 +98,7 @@
                 buffer.append("\n");
             }
 
-            mTestCase.fail("Expected line(s) not found:" + buffer.toString());
+            TestCase.fail("Expected line(s) not found:" + buffer.toString());
         }
     }
 }
diff --git a/core/tests/coretests/src/android/pim/vcard/PropertyNode.java b/core/tests/coretests/src/android/pim/vcard/test_utils/PropertyNode.java
similarity index 95%
rename from core/tests/coretests/src/android/pim/vcard/PropertyNode.java
rename to core/tests/coretests/src/android/pim/vcard/test_utils/PropertyNode.java
index 2c1f6d2..de7ad8e 100644
--- a/core/tests/coretests/src/android/pim/vcard/PropertyNode.java
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/PropertyNode.java
@@ -13,11 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.pim.vcard;
+package android.pim.vcard.test_utils;
 
 import android.content.ContentValues;
 import android.pim.vcard.VCardEntry;
-import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -26,12 +25,18 @@
 import java.util.Set;
 
 /**
+ * <p>
+ * The class representing one property (e.g. "N;ENCODING=UTF-8:family:given:middle:prefix:suffix").
+ * </p>
+ * <p>
  * Previously used in main vCard handling code but now exists only for testing.
- *
+ * </p>
+ * <p>
  * Especially useful for testing parser code (VCardParser), since all properties can be
  * checked via this class unlike {@link VCardEntry}, which only emits the result of
  * interpretation of the content of each vCard. We cannot know whether vCard parser or
- * ContactStruct is wrong withouth this class.
+ * {@link VCardEntry} is wrong without this class.
+ * </p>
  */
 public class PropertyNode {
     public String propName;
@@ -43,7 +48,8 @@
      */
     public byte[] propValue_bytes;
 
-    /** param store: key=paramType, value=paramValue
+    /**
+     * param store: key=paramType, value=paramValue
      * Note that currently PropertyNode class does not support multiple param-values
      * defined in vCard 3.0 (See also RFC 2426). multiple-values are stored as
      * one String value like "A,B", not ["A", "B"]...
diff --git a/core/tests/coretests/src/android/pim/vcard/test_utils/PropertyNodesVerifier.java b/core/tests/coretests/src/android/pim/vcard/test_utils/PropertyNodesVerifier.java
new file mode 100644
index 0000000..4229284
--- /dev/null
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/PropertyNodesVerifier.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2009 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.test_utils;
+
+import android.pim.vcard.VCardParser;
+import android.pim.vcard.VCardUtils;
+import android.pim.vcard.exception.VCardException;
+import android.test.AndroidTestCase;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PropertyNodesVerifier extends VNodeBuilder {
+    private final List<PropertyNodesVerifierElem> mPropertyNodesVerifierElemList;
+    private final AndroidTestCase mAndroidTestCase;
+    private int mIndex;
+
+    public PropertyNodesVerifier(AndroidTestCase testCase) {
+        super();
+        mPropertyNodesVerifierElemList = new ArrayList<PropertyNodesVerifierElem>();
+        mAndroidTestCase = testCase;
+    }
+
+    public PropertyNodesVerifierElem addPropertyNodesVerifierElem() {
+        PropertyNodesVerifierElem elem = new PropertyNodesVerifierElem(mAndroidTestCase);
+        mPropertyNodesVerifierElemList.add(elem);
+        return elem;
+    }
+
+    public void verify(int resId, int vcardType) throws IOException, VCardException {
+        verify(mAndroidTestCase.getContext().getResources().openRawResource(resId), vcardType);
+    }
+
+    public void verify(int resId, int vcardType, final VCardParser parser)
+            throws IOException, VCardException {
+        verify(mAndroidTestCase.getContext().getResources().openRawResource(resId),
+                vcardType, parser);
+    }
+
+    public void verify(InputStream is, int vcardType) throws IOException, VCardException {
+        final VCardParser parser = VCardUtils.getAppropriateParser(vcardType);
+        verify(is, vcardType, parser);
+    }
+
+    public void verify(InputStream is, int vcardType, final VCardParser parser)
+            throws IOException, VCardException {
+        try {
+            parser.parse(is, this);
+        } finally {
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+    }
+
+    @Override
+    public void endEntry() {
+        super.endEntry();
+        AndroidTestCase.assertTrue(mIndex < mPropertyNodesVerifierElemList.size());
+        AndroidTestCase.assertTrue(mIndex < vNodeList.size());
+        mPropertyNodesVerifierElemList.get(mIndex).verify(vNodeList.get(mIndex));
+        mIndex++;
+    }
+}
diff --git a/core/tests/coretests/src/android/pim/vcard/PropertyNodesVerifier.java b/core/tests/coretests/src/android/pim/vcard/test_utils/PropertyNodesVerifierElem.java
similarity index 77%
rename from core/tests/coretests/src/android/pim/vcard/PropertyNodesVerifier.java
rename to core/tests/coretests/src/android/pim/vcard/test_utils/PropertyNodesVerifierElem.java
index cfdd074..44c6abc 100644
--- a/core/tests/coretests/src/android/pim/vcard/PropertyNodesVerifier.java
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/PropertyNodesVerifierElem.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 The Android Open Source Project
+ * 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.
@@ -13,87 +13,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.pim.vcard;
+package android.pim.vcard.test_utils;
 
 import android.content.ContentValues;
-import android.pim.vcard.VCardConfig;
-import android.pim.vcard.VCardParser;
-import android.pim.vcard.VCardParser_V21;
-import android.pim.vcard.VCardParser_V30;
-import android.pim.vcard.exception.VCardException;
 import android.test.AndroidTestCase;
 
 import junit.framework.TestCase;
 
-import java.io.IOException;
-import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 
-/* package */ class PropertyNodesVerifier extends VNodeBuilder {
-    private final List<PropertyNodesVerifierElem> mPropertyNodesVerifierElemList;
-    private final AndroidTestCase mAndroidTestCase;
-    private int mIndex;
-
-    public PropertyNodesVerifier(AndroidTestCase testCase) {
-        mPropertyNodesVerifierElemList = new ArrayList<PropertyNodesVerifierElem>();
-        mAndroidTestCase = testCase;
-    }
-
-    public PropertyNodesVerifierElem addPropertyNodesVerifierElem() {
-        PropertyNodesVerifierElem elem = new PropertyNodesVerifierElem(mAndroidTestCase);
-        mPropertyNodesVerifierElemList.add(elem);
-        return elem;
-    }
-
-    public void verify(int resId, int vCardType)
-            throws IOException, VCardException {
-        verify(mAndroidTestCase.getContext().getResources().openRawResource(resId), vCardType);
-    }
-
-    public void verify(int resId, int vCardType, final VCardParser vCardParser)
-            throws IOException, VCardException {
-        verify(mAndroidTestCase.getContext().getResources().openRawResource(resId),
-                vCardType, vCardParser);
-    }
-
-    public void verify(InputStream is, int vCardType) throws IOException, VCardException {
-        final VCardParser vCardParser;
-        if (VCardConfig.isV30(vCardType)) {
-            vCardParser = new VCardParser_V30(true);  // Use StrictParsing.
-        } else {
-            vCardParser = new VCardParser_V21();
-        }
-        verify(is, vCardType, vCardParser);
-    }
-
-    public void verify(InputStream is, int vCardType, final VCardParser vCardParser)
-            throws IOException, VCardException {
-        try {
-            vCardParser.parse(is, this);
-        } finally {
-            if (is != null) {
-                try {
-                    is.close();
-                } catch (IOException e) {
-                }
-            }
-        }
-    }
-
-    @Override
-    public void endEntry() {
-        super.endEntry();
-        mAndroidTestCase.assertTrue(mIndex < mPropertyNodesVerifierElemList.size());
-        mAndroidTestCase.assertTrue(mIndex < vNodeList.size());
-        mPropertyNodesVerifierElemList.get(mIndex).verify(vNodeList.get(mIndex));
-        mIndex++;
-    }
-}
-
 /**
  * Utility class which verifies input VNode.
  *
@@ -102,7 +34,7 @@
  * If the node does not exist in the "ordered list", the class refers to
  * "unorderd expected property set" and checks the node is expected somewhere.
  */
-/* package */ class PropertyNodesVerifierElem {
+public class PropertyNodesVerifierElem {
     public static class TypeSet extends HashSet<String> {
         public TypeSet(String ... array) {
             super(Arrays.asList(array));
@@ -119,12 +51,10 @@
     // Intentionally use ArrayList instead of Set, assuming there may be more than one
     // exactly same objects.
     private final ArrayList<PropertyNode> mUnorderedNodeList;
-    private final TestCase mTestCase;
 
-    public PropertyNodesVerifierElem(TestCase testCase) {
+    public PropertyNodesVerifierElem(AndroidTestCase androidTestCase) {
         mOrderedNodeMap = new HashMap<String, List<PropertyNode>>();
         mUnorderedNodeList = new ArrayList<PropertyNode>();
-        mTestCase = testCase;
     }
 
     // WithOrder
@@ -170,6 +100,12 @@
                 paramMap_TYPE, null);
     }
 
+    public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName,
+            List<String> propValueList, ContentValues paramMap, TypeSet paramMap_TYPE) {
+        return addExpectedNodeWithOrder(propName, null, propValueList, null, paramMap,
+                paramMap_TYPE, null);
+    }
+    
     public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue,
             ContentValues paramMap, TypeSet paramMap_TYPE) {
         return addExpectedNodeWithOrder(propName, propValue, null, null,
@@ -183,12 +119,18 @@
     }
 
     public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue,
+            List<String> propValueList, ContentValues paramMap) {
+        return addExpectedNodeWithOrder(propName, propValue, propValueList, null, paramMap,
+                null, null);
+    }
+
+    public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue,
             List<String> propValueList, byte[] propValue_bytes,
             ContentValues paramMap, TypeSet paramMap_TYPE, GroupSet propGroupSet) {
         if (propValue == null && propValueList != null) {
             propValue = concatinateListWithSemiColon(propValueList);
         }
-        PropertyNode propertyNode = new PropertyNode(propName,
+        final PropertyNode propertyNode = new PropertyNode(propName,
                 propValue, propValueList, propValue_bytes,
                 paramMap, paramMap_TYPE, propGroupSet);
         List<PropertyNode> expectedNodeList = mOrderedNodeMap.get(propName);
@@ -267,8 +209,9 @@
         for (PropertyNode actualNode : vnode.propList) {
             verifyNode(actualNode.propName, actualNode);
         }
+
         if (!mOrderedNodeMap.isEmpty() || !mUnorderedNodeList.isEmpty()) {
-            List<String> expectedProps = new ArrayList<String>();
+            final List<String> expectedProps = new ArrayList<String>();
             for (List<PropertyNode> nodes : mOrderedNodeMap.values()) {
                 for (PropertyNode node : nodes) {
                     if (!expectedProps.contains(node.propName)) {
@@ -281,18 +224,19 @@
                     expectedProps.add(node.propName);
                 }
             }
-            mTestCase.fail("Expected property " + Arrays.toString(expectedProps.toArray())
+            TestCase.fail("Expected property " + Arrays.toString(expectedProps.toArray())
                     + " was not found.");
         }
     }
 
     private void verifyNode(final String propName, final PropertyNode actualNode) {
-        List<PropertyNode> expectedNodeList = mOrderedNodeMap.get(propName);
+        final List<PropertyNode> expectedNodeList = mOrderedNodeMap.get(propName);
         final int size = (expectedNodeList != null ? expectedNodeList.size() : 0);
         if (size > 0) {
             for (int i = 0; i < size; i++) {
-                PropertyNode expectedNode = expectedNodeList.get(i);
-                List<PropertyNode> expectedButDifferentValueList = new ArrayList<PropertyNode>();
+                final PropertyNode expectedNode = expectedNodeList.get(i);
+                final List<PropertyNode> expectedButDifferentValueList =
+                        new ArrayList<PropertyNode>();
                 if (expectedNode.propName.equals(propName)) {
                     if (expectedNode.equals(actualNode)) {
                         expectedNodeList.remove(i);
@@ -318,7 +262,7 @@
                             expectedButDifferentValueList);
                 } else {
                     // There's no expected node with same propName.
-                    mTestCase.fail("Unexpected property \"" + propName + "\" exists.");
+                    TestCase.fail("Unexpected property \"" + propName + "\" exists.");
                 }
             }
         } else {
@@ -333,7 +277,7 @@
                             expectedButDifferentValueList);
                 } else {
                     // There's no expected node with same propName.
-                    mTestCase.fail("Unexpected property \"" + propName + "\" exists.");
+                    TestCase.fail("Unexpected property \"" + propName + "\" exists.");
                 }
             }
         }
@@ -379,7 +323,7 @@
             builder.append(expectedNode.toString());
             builder.append("\n");
         }
-        mTestCase.fail("Property \"" + propName + "\" has wrong value.\n"
+        TestCase.fail("Property \"" + propName + "\" has wrong value.\n"
                 + builder.toString()
                 + "  actual: " + actualNode.toString());
     }
diff --git a/core/tests/coretests/src/android/pim/vcard/VCardTestsBase.java b/core/tests/coretests/src/android/pim/vcard/test_utils/VCardTestsBase.java
similarity index 92%
rename from core/tests/coretests/src/android/pim/vcard/VCardTestsBase.java
rename to core/tests/coretests/src/android/pim/vcard/test_utils/VCardTestsBase.java
index 9c6003f..e7413ab 100644
--- a/core/tests/coretests/src/android/pim/vcard/VCardTestsBase.java
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/VCardTestsBase.java
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package android.pim.vcard;
+package android.pim.vcard.test_utils;
 
 import android.content.ContentValues;
+import android.pim.vcard.VCardConfig;
 import android.test.AndroidTestCase;
 
 /**
@@ -24,8 +24,9 @@
  * Please do not add each unit test here.
  */
 public class VCardTestsBase extends AndroidTestCase {
-    public static final int V21 = VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8;
-    public static final int V30 = VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8;
+    public static final int V21 = VCardConfig.VCARD_TYPE_V21_GENERIC;
+    public static final int V30 = VCardConfig.VCARD_TYPE_V30_GENERIC;
+    public static final int V40 = VCardConfig.VCARD_TYPE_V40_GENERIC;
 
     // Do not modify these during tests.
     protected final ContentValues mContentValuesForQP;
@@ -41,6 +42,7 @@
 
     public VCardTestsBase() {
         super();
+        // Not using constants in vCard code since it may be wrong.
         mContentValuesForQP = new ContentValues();
         mContentValuesForQP.put("ENCODING", "QUOTED-PRINTABLE");
         mContentValuesForSJis = new ContentValues();
diff --git a/core/tests/coretests/src/android/pim/vcard/test_utils/VCardVerifier.java b/core/tests/coretests/src/android/pim/vcard/test_utils/VCardVerifier.java
new file mode 100644
index 0000000..7379a5b
--- /dev/null
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/VCardVerifier.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2009 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.test_utils;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.EntityIterator;
+import android.net.Uri;
+import android.pim.vcard.VCardComposer;
+import android.pim.vcard.VCardConfig;
+import android.pim.vcard.VCardEntryConstructor;
+import android.pim.vcard.VCardInterpreter;
+import android.pim.vcard.VCardInterpreterCollection;
+import android.pim.vcard.VCardParser;
+import android.pim.vcard.VCardUtils;
+import android.pim.vcard.exception.VCardException;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContext;
+import android.text.TextUtils;
+import android.util.Log;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+/**
+ * <p>
+ * The class lets users checks that given expected vCard data are same as given actual vCard data.
+ * Able to verify both vCard importer/exporter.
+ * </p>
+ * <p>
+ * First a user has to initialize the object by calling either
+ * {@link #initForImportTest(int, int)} or {@link #initForExportTest(int)}.
+ * "Round trip test" (import -> export -> import, or export -> import -> export) is not supported.
+ * </p>
+ */
+public class VCardVerifier {
+    private static final String LOG_TAG = "VCardVerifier";
+
+    private static class CustomMockContext extends MockContext {
+        final ContentResolver mResolver;
+        public CustomMockContext(ContentResolver resolver) {
+            mResolver = resolver;
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return mResolver;
+        }
+    }
+
+    private class VCardVerifierInternal implements VCardComposer.OneEntryHandler {
+        public boolean onInit(Context context) {
+            return true;
+        }
+        public boolean onEntryCreated(String vcard) {
+            verifyOneVCardForExport(vcard);
+            return true;
+        }
+        public void onTerminate() {
+        }
+    }
+
+    private final AndroidTestCase mAndroidTestCase;
+    private final VCardVerifierInternal mVCardVerifierInternal;
+    private int mVCardType;
+    private boolean mIsDoCoMo;
+
+    // Only one of them must be non-empty.
+    private ExportTestResolver mExportTestResolver;
+    private InputStream mInputStream;
+
+    // To allow duplication, use list instead of set.
+    // When null, we don't need to do the verification.
+    private PropertyNodesVerifier mPropertyNodesVerifier;
+    private LineVerifier mLineVerifier;
+    private ContentValuesVerifier mContentValuesVerifier;
+    private boolean mInitialized;
+    private boolean mVerified = false;
+    private String mCharset;
+
+    // Called by VCardTestsBase
+    public VCardVerifier(AndroidTestCase androidTestCase) {
+        mAndroidTestCase = androidTestCase;
+        mVCardVerifierInternal = new VCardVerifierInternal();
+        mExportTestResolver = null;
+        mInputStream = null;
+        mInitialized = false;
+        mVerified = false;
+    }
+
+    // Should be called at the beginning of each import test.
+    public void initForImportTest(int vcardType, int resId) {
+        if (mInitialized) {
+            AndroidTestCase.fail("Already initialized");
+        }
+        mVCardType = vcardType;
+        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
+        setInputResourceId(resId);
+        mInitialized = true;
+    }
+
+    // Should be called at the beginning of each export test.
+    public void initForExportTest(int vcardType) {
+        initForExportTest(vcardType, "UTF-8");
+    }
+
+    public void initForExportTest(int vcardType, String charset) {
+        if (mInitialized) {
+            AndroidTestCase.fail("Already initialized");
+        }
+        mExportTestResolver = new ExportTestResolver(mAndroidTestCase);
+        mVCardType = vcardType;
+        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
+        mInitialized = true;
+        if (TextUtils.isEmpty(charset)) {
+            mCharset = "UTF-8";
+        } else {
+            mCharset = charset;
+        }
+    }
+
+    private void setInputResourceId(int resId) {
+        InputStream inputStream = mAndroidTestCase.getContext().getResources().openRawResource(resId);
+        if (inputStream == null) {
+            AndroidTestCase.fail("Wrong resId: " + resId);
+        }
+        setInputStream(inputStream);
+    }
+
+    private void setInputStream(InputStream inputStream) {
+        if (mExportTestResolver != null) {
+            AndroidTestCase.fail("addInputEntry() is called.");
+        } else if (mInputStream != null) {
+            AndroidTestCase.fail("InputStream is already set");
+        }
+        mInputStream = inputStream;
+    }
+
+    public ContactEntry addInputEntry() {
+        if (!mInitialized) {
+            AndroidTestCase.fail("Not initialized");
+        }
+        if (mInputStream != null) {
+            AndroidTestCase.fail("setInputStream is called");
+        }
+        return mExportTestResolver.addInputContactEntry();
+    }
+
+    public PropertyNodesVerifierElem addPropertyNodesVerifierElemWithoutVersion() {
+        if (!mInitialized) {
+            AndroidTestCase.fail("Not initialized");
+        }
+        if (mPropertyNodesVerifier == null) {
+            mPropertyNodesVerifier = new PropertyNodesVerifier(mAndroidTestCase);
+        }
+        return mPropertyNodesVerifier.addPropertyNodesVerifierElem();
+    }
+    
+    public PropertyNodesVerifierElem addPropertyNodesVerifierElem() {
+        final PropertyNodesVerifierElem elem = addPropertyNodesVerifierElemWithoutVersion();
+        final String versionString;
+        if (VCardConfig.isVersion21(mVCardType)) {
+            versionString = "2.1";
+        } else if (VCardConfig.isVersion30(mVCardType)) {
+            versionString = "3.0";
+        } else if (VCardConfig.isVersion40(mVCardType)) {
+            versionString = "4.0";
+        } else {
+            throw new RuntimeException("Unexpected vcard type during a unit test");
+        }
+        elem.addExpectedNodeWithOrder("VERSION", versionString);
+
+        return elem;
+    }
+
+    public PropertyNodesVerifierElem addPropertyNodesVerifierElemWithEmptyName() {
+        if (!mInitialized) {
+            AndroidTestCase.fail("Not initialized");
+        }
+        final PropertyNodesVerifierElem elem = addPropertyNodesVerifierElem();
+        if (VCardConfig.isVersion40(mVCardType)) {
+            elem.addExpectedNodeWithOrder("FN", "");
+        } else if (VCardConfig.isVersion30(mVCardType)) {
+            elem.addExpectedNodeWithOrder("N", "");
+            elem.addExpectedNodeWithOrder("FN", "");
+        } else if (mIsDoCoMo) {
+            elem.addExpectedNodeWithOrder("N", "");
+        }
+        return elem;
+    }
+
+    public LineVerifierElem addLineVerifierElem() {
+        if (!mInitialized) {
+            AndroidTestCase.fail("Not initialized");
+        }
+        if (mLineVerifier == null) {
+            mLineVerifier = new LineVerifier(mAndroidTestCase, mVCardType);
+        }
+        return mLineVerifier.addLineVerifierElem();
+    }
+
+    public ContentValuesVerifierElem addContentValuesVerifierElem() {
+        if (!mInitialized) {
+            AndroidTestCase.fail("Not initialized");
+        }
+        if (mContentValuesVerifier == null) {
+            mContentValuesVerifier = new ContentValuesVerifier();
+        }
+
+        return mContentValuesVerifier.addElem(mAndroidTestCase);
+    }
+
+    /**
+     * Sets up sub-verifiers correctly and try parse given vCard as InputStream.
+     * Errors around InputStream must be handled outside this method.
+     *
+     * Used both from {@link #verifyForImportTest()} and from {@link #verifyForExportTest()}.
+     */
+    private void verifyWithInputStream(InputStream is) throws IOException {
+        final VCardInterpreter interpreter;
+        if (mContentValuesVerifier != null) {
+            final VCardEntryConstructor constructor = new VCardEntryConstructor(mVCardType);
+            constructor.addEntryHandler(mContentValuesVerifier);
+            if (mPropertyNodesVerifier != null) {
+                interpreter = new VCardInterpreterCollection(Arrays.asList(
+                        mPropertyNodesVerifier, constructor));
+            } else {
+                interpreter = constructor;
+            }
+        } else {
+            if (mPropertyNodesVerifier != null) {
+                interpreter = mPropertyNodesVerifier;
+            } else {
+                interpreter = null;
+            }
+        }
+
+        try {
+            // Note: we must not specify charset toward vCard parsers. This code checks whether
+            // those parsers are able to encode given binary without any extra information for
+            // charset.
+            final VCardParser parser = VCardUtils.getAppropriateParser(mVCardType);
+            parser.parse(is, interpreter);
+        } catch (VCardException e) {
+            AndroidTestCase.fail("Unexpected VCardException: " + e.getMessage());
+        }
+    }
+
+    private void verifyOneVCardForExport(final String vcard) {
+        Log.d(LOG_TAG, vcard);
+        InputStream is = null;
+        try {
+            is = new ByteArrayInputStream(vcard.getBytes(mCharset));
+            verifyWithInputStream(is);
+        } catch (IOException e) {
+            AndroidTestCase.fail("Unexpected IOException: " + e.getMessage());
+        } finally {
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                    AndroidTestCase.fail("Unexpected IOException: " + e.getMessage());
+                }
+            }
+        }
+    }
+
+    public void verify() {
+        if (!mInitialized) {
+            TestCase.fail("Not initialized.");
+        }
+        if (mVerified) {
+            TestCase.fail("verify() was called twice.");
+        }
+
+        if (mInputStream != null) {
+            if (mExportTestResolver != null){
+                TestCase.fail("There are two input sources.");
+            }
+            verifyForImportTest();
+        } else if (mExportTestResolver != null){
+            verifyForExportTest();
+        } else {
+            TestCase.fail("No input is determined");
+        }
+        mVerified = true;
+    }
+
+    private void verifyForImportTest() {
+        if (mLineVerifier != null) {
+            AndroidTestCase.fail("Not supported now.");
+        }
+
+        try {
+            verifyWithInputStream(mInputStream);
+        } catch (IOException e) {
+            AndroidTestCase.fail("IOException was thrown: " + e.getMessage());
+        } finally {
+            if (mInputStream != null) {
+                try {
+                    mInputStream.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+    }
+
+    public static EntityIterator mockGetEntityIteratorMethod(
+            final ContentResolver resolver,
+            final Uri uri, final String selection,
+            final String[] selectionArgs, final String sortOrder) {
+        if (ExportTestResolver.class.equals(resolver.getClass())) {
+            return ((ExportTestResolver)resolver).getProvider().queryEntities(
+                    uri, selection, selectionArgs, sortOrder);
+        }
+
+        Log.e(LOG_TAG, "Unexpected provider given.");
+        return null;
+    }
+
+    private Method getMockGetEntityIteratorMethod()
+            throws SecurityException, NoSuchMethodException {
+        return this.getClass().getMethod("mockGetEntityIteratorMethod",
+                ContentResolver.class, Uri.class, String.class, String[].class, String.class);
+    }
+
+    private void verifyForExportTest() {
+       final VCardComposer composer =
+            new VCardComposer(new CustomMockContext(mExportTestResolver), mVCardType, mCharset);
+        composer.addHandler(mLineVerifier);
+        composer.addHandler(mVCardVerifierInternal);
+        if (!composer.init(VCardComposer.CONTACTS_TEST_CONTENT_URI, null, null, null)) {
+            AndroidTestCase.fail("init() failed. Reason: " + composer.getErrorReason());
+        }
+        AndroidTestCase.assertFalse(composer.isAfterLast());
+        try {
+            while (!composer.isAfterLast()) {
+                try {
+                    final Method mockGetEntityIteratorMethod = getMockGetEntityIteratorMethod();
+                    AndroidTestCase.assertNotNull(mockGetEntityIteratorMethod);
+                    AndroidTestCase.assertTrue(
+                            composer.createOneEntry(mockGetEntityIteratorMethod));
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    AndroidTestCase.fail(e.toString());
+                }
+            }
+        } finally {
+            composer.terminate();
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/pim/vcard/VNode.java b/core/tests/coretests/src/android/pim/vcard/test_utils/VNode.java
similarity index 95%
rename from core/tests/coretests/src/android/pim/vcard/VNode.java
rename to core/tests/coretests/src/android/pim/vcard/test_utils/VNode.java
index 79f10dc..b890e2c 100644
--- a/core/tests/coretests/src/android/pim/vcard/VNode.java
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/VNode.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.pim.vcard;
+package android.pim.vcard.test_utils;
 
 import java.util.ArrayList;
 
diff --git a/core/tests/coretests/src/android/pim/vcard/VNodeBuilder.java b/core/tests/coretests/src/android/pim/vcard/test_utils/VNodeBuilder.java
similarity index 62%
rename from core/tests/coretests/src/android/pim/vcard/VNodeBuilder.java
rename to core/tests/coretests/src/android/pim/vcard/test_utils/VNodeBuilder.java
index 0e6c325..8837034 100644
--- a/core/tests/coretests/src/android/pim/vcard/VNodeBuilder.java
+++ b/core/tests/coretests/src/android/pim/vcard/test_utils/VNodeBuilder.java
@@ -13,18 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.pim.vcard;
+package android.pim.vcard.test_utils;
 
 import android.content.ContentValues;
-import android.pim.vcard.VCardInterpreter;
 import android.pim.vcard.VCardConfig;
+import android.pim.vcard.VCardInterpreter;
+import android.pim.vcard.VCardUtils;
+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;
@@ -32,34 +30,29 @@
 import java.util.List;
 
 /**
- * Store the parse result to custom datastruct: VNode, PropertyNode
+ * <p>
+ * The class storing the parse result to custom datastruct:
+ * {@link VNode}, and {@link PropertyNode}.
  * Maybe several vcard instance, so use vNodeList to store.
- * VNode: standy by a vcard instance.
- * PropertyNode: standy by a property line of a card.
- *
- * Previously used in main vCard handling code but now exists only for testing.
+ * </p>
+ * <p>
+ * This is called VNode, not VCardNode, since it was used for expressing vCalendar (iCal).
+ * </p>
  */
 public class VNodeBuilder implements VCardInterpreter {
     static private String LOG_TAG = "VNodeBuilder"; 
     
-    /**
-     * If there's no other information available, this class uses this charset for encoding
-     * byte arrays.
-     */
-    static public String TARGET_CHARSET = "UTF-8"; 
-    
-    /** type=VNode */
     public List<VNode> vNodeList = new ArrayList<VNode>();
     private int mNodeListPos = 0;
     private VNode mCurrentVNode;
     private PropertyNode mCurrentPropNode;
     private String mCurrentParamType;
-    
+
     /**
      * The charset using which VParser parses the text.
      */
     private String mSourceCharset;
-    
+
     /**
      * The charset with which byte array is encoded to String.
      */
@@ -68,27 +61,15 @@
     private boolean mStrictLineBreakParsing;
     
     public VNodeBuilder() {
-        this(VCardConfig.DEFAULT_CHARSET, TARGET_CHARSET, false);
+        this(VCardConfig.DEFAULT_IMPORT_CHARSET, false);
     }
 
-    public VNodeBuilder(String charset, boolean strictLineBreakParsing) {
-        this(null, charset, strictLineBreakParsing);
-    }
-    
-    /**
-     * @hide sourceCharset is temporal. 
-     */
-    public VNodeBuilder(String sourceCharset, String targetCharset,
-            boolean strictLineBreakParsing) {
-        if (sourceCharset != null) {
-            mSourceCharset = sourceCharset;
-        } else {
-            mSourceCharset = VCardConfig.DEFAULT_CHARSET;
-        }
+    public VNodeBuilder(String targetCharset, boolean strictLineBreakParsing) {
+        mSourceCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET;
         if (targetCharset != null) {
             mTargetCharset = targetCharset;
         } else {
-            mTargetCharset = TARGET_CHARSET;
+            mTargetCharset = VCardConfig.DEFAULT_IMPORT_CHARSET;
         }
         mStrictLineBreakParsing = strictLineBreakParsing;
     }
@@ -149,7 +130,6 @@
         mCurrentPropNode.propName = name;
     }
 
-    // Used only in VCard.
     public void propertyGroup(String group) {
         mCurrentPropNode.propGroupSet.add(group);
     }
@@ -159,6 +139,12 @@
     }
 
     public void propertyParamValue(String value) {
+        if (!VCardUtils.containsOnlyAlphaDigitHyphen(value)) {
+            value = VCardUtils.convertStringCharset(value,
+                    VCardConfig.DEFAULT_INTERMEDIATE_CHARSET,
+                    VCardConfig.DEFAULT_IMPORT_CHARSET);
+        }
+
         if (mCurrentParamType == null ||
                 mCurrentParamType.equalsIgnoreCase("TYPE")) {
             mCurrentPropNode.paramMap_TYPE.add(value);
@@ -192,71 +178,11 @@
             encoding = encoding.toUpperCase();
             if (encoding.equals("BASE64") || encoding.equals("B")) {
                 // Assume BASE64 is used only when the number of values is 1.
-                mCurrentPropNode.propValue_bytes =
-                    Base64.decodeBase64(value.getBytes());
+                mCurrentPropNode.propValue_bytes = Base64.decode(value.getBytes(), Base64.NO_WRAP);
                 return value;
             } else if (encoding.equals("QUOTED-PRINTABLE")) {
-                String quotedPrintable = value
-                .replaceAll("= ", " ").replaceAll("=\t", "\t");
-                String[] lines;
-                if (mStrictLineBreakParsing) {
-                    lines = quotedPrintable.split("\r\n");
-                } else {
-                    StringBuilder builder = new StringBuilder();
-                    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);
-                        }
-                    }
-                    String finalLine = builder.toString();
-                    if (finalLine.length() > 0) {
-                        list.add(finalLine);
-                    }
-                    lines = list.toArray(new String[0]);
-                }
-                StringBuilder 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(mSourceCharset);
-                } catch (UnsupportedEncodingException e1) {
-                    Log.e(LOG_TAG, "Failed to encode: charset=" + mSourceCharset);
-                    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, targetCharset);
-                } catch (UnsupportedEncodingException e) {
-                    Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset);
-                    return new String(bytes);
-                }
+                return VCardUtils.parseQuotedPrintable(
+                        value, mStrictLineBreakParsing, mSourceCharset, targetCharset);
             }
             // Unknown encoding. Fall back to default.
         }
@@ -309,6 +235,6 @@
     }
     
     public String getResult(){
-        return null;
+        throw new RuntimeException("Not supported");
     }
 }
diff --git a/docs/html/guide/topics/resources/more-resources.jd b/docs/html/guide/topics/resources/more-resources.jd
index a647571..6cae1eb 100644
--- a/docs/html/guide/topics/resources/more-resources.jd
+++ b/docs/html/guide/topics/resources/more-resources.jd
@@ -216,10 +216,13 @@
 For example: 10px, 2in, 5sp. The following units of measure are supported by Android:</p>
 <dl>
   <dt>{@code dp}</dt>
-    <dd>Density-independent Pixels - an abstract unit that is based on the physical density of the screen.
-    These units are relative to a 160 dpi screen, so one dp is one pixel on a 160 dpi screen. The ratio of
-    dp-to-pixel will change with the screen density, but not necessarily in direct proportion. The
-      compiler accepts both "dip" and "dp", though "dp" is more consistent with "sp".</dd>
+    <dd>Density-independent Pixels - an abstract unit that is based on the physical density of the
+screen. These units are relative to a 160 dpi (dots per inch) screen, so <em>{@code 160dp} is
+always one inch</em> regardless of the screen density. The ratio of dp-to-pixel will change with the
+screen density, but not necessarily in direct proportion. You should use these units when specifying
+view dimensions in your layout, so the UI properly scales to render at the same actual size on
+different screens. (The compiler accepts both "dip" and "dp", though "dp" is more consistent with
+"sp".)</dd>
   <dt>{@code sp}</dt>
     <dd>Scale-independent Pixels - this is like the dp unit, but it is also scaled by the user's font
     size preference. It is recommend you use this unit when specifying font sizes, so they will be adjusted
diff --git a/include/media/stagefright/AudioSource.h b/include/media/stagefright/AudioSource.h
index 1af4254..a5cec78 100644
--- a/include/media/stagefright/AudioSource.h
+++ b/include/media/stagefright/AudioSource.h
@@ -72,6 +72,7 @@
     int64_t mPrevSampleTimeUs;
     int64_t mTotalLostFrames;
     int64_t mPrevLostBytes;
+    int64_t mInitialReadTimeUs;
 
     MediaBufferGroup *mGroup;
 
diff --git a/include/media/stagefright/MPEG4Writer.h b/include/media/stagefright/MPEG4Writer.h
index 2412f6a..9716e98 100644
--- a/include/media/stagefright/MPEG4Writer.h
+++ b/include/media/stagefright/MPEG4Writer.h
@@ -132,7 +132,7 @@
     // Adjust other track media clock (presumably wall clock)
     // based on audio track media clock with the drift time.
     int64_t mDriftTimeUs;
-    void addDriftTimeUs(int64_t driftTimeUs);
+    void setDriftTimeUs(int64_t driftTimeUs);
     int64_t getDriftTimeUs();
 
     void lock();
diff --git a/include/media/stagefright/MetaData.h b/include/media/stagefright/MetaData.h
index 43354c2..3b31e68 100644
--- a/include/media/stagefright/MetaData.h
+++ b/include/media/stagefright/MetaData.h
@@ -48,6 +48,7 @@
     kKeyTime              = 'time',  // int64_t (usecs)
     kKeyNTPTime           = 'ntpT',  // uint64_t (ntp-timestamp)
     kKeyTargetTime        = 'tarT',  // int64_t (usecs)
+    kKeyDriftTime         = 'dftT',  // int64_t (usecs)
     kKeyDuration          = 'dura',  // int64_t (usecs)
     kKeyColorFormat       = 'colf',
     kKeyPlatformPrivate   = 'priv',  // pointer
diff --git a/libs/utils/ResourceTypes.cpp b/libs/utils/ResourceTypes.cpp
index a1401ad..8345cc3 100644
--- a/libs/utils/ResourceTypes.cpp
+++ b/libs/utils/ResourceTypes.cpp
@@ -1934,8 +1934,8 @@
         ssize_t offset = getEntry(package, t, e, &mParams, &type, &entry, &typeClass);
         if (offset <= 0) {
             if (offset < 0) {
-                LOGW("Failure getting entry for 0x%08x (t=%d e=%d) in package %d: 0x%08x\n",
-                        resID, t, e, (int)ip, (int)offset);
+                LOGW("Failure getting entry for 0x%08x (t=%d e=%d) in package %zd (error %d)\n",
+                        resID, t, e, ip, (int)offset);
                 return offset;
             }
             continue;
diff --git a/media/libstagefright/AudioSource.cpp b/media/libstagefright/AudioSource.cpp
index bcae913..c2f79e8 100644
--- a/media/libstagefright/AudioSource.cpp
+++ b/media/libstagefright/AudioSource.cpp
@@ -84,6 +84,7 @@
 
     mTrackMaxAmplitude = false;
     mMaxAmplitude = 0;
+    mInitialReadTimeUs = 0;
     mStartTimeUs = 0;
     int64_t startTimeUs;
     if (params && params->findInt64(kKeyTime, &startTimeUs)) {
@@ -210,6 +211,7 @@
         return NO_INIT;
     }
 
+    int64_t readTimeUs = systemTime() / 1000;
     *out = NULL;
 
     MediaBuffer *buffer;
@@ -223,9 +225,10 @@
 
 
         if (numFramesRecorded == 0 && mPrevSampleTimeUs == 0) {
+            mInitialReadTimeUs = readTimeUs;
             // Initial delay
             if (mStartTimeUs > 0) {
-                mStartTimeUs = systemTime() / 1000 - mStartTimeUs;
+                mStartTimeUs = readTimeUs - mStartTimeUs;
             } else {
                 // Assume latency is constant.
                 mStartTimeUs += mRecord->latency() * 1000;
@@ -271,7 +274,10 @@
             }
             memset(buffer->data(), 0, numLostBytes);
             buffer->set_range(0, numLostBytes);
-            buffer->meta_data()->setInt64(kKeyTime, mPrevSampleTimeUs);
+            if (numFramesRecorded == 0) {
+                buffer->meta_data()->setInt64(kKeyTime, mStartTimeUs);
+            }
+            buffer->meta_data()->setInt64(kKeyDriftTime, readTimeUs - mInitialReadTimeUs);
             mPrevSampleTimeUs = timestampUs;
             *out = buffer;
             return OK;
@@ -309,7 +315,10 @@
             trackMaxAmplitude((int16_t *) buffer->data(), n >> 1);
         }
 
-        buffer->meta_data()->setInt64(kKeyTime, mPrevSampleTimeUs);
+        if (numFramesRecorded == 0) {
+            buffer->meta_data()->setInt64(kKeyTime, mStartTimeUs);
+        }
+        buffer->meta_data()->setInt64(kKeyDriftTime, readTimeUs - mInitialReadTimeUs);
         CHECK(timestampUs > mPrevSampleTimeUs);
         mPrevSampleTimeUs = timestampUs;
         LOGV("initial delay: %lld, sample rate: %d, timestamp: %lld",
diff --git a/media/libstagefright/MPEG4Writer.cpp b/media/libstagefright/MPEG4Writer.cpp
index a15b84e..af2b4c4 100644
--- a/media/libstagefright/MPEG4Writer.cpp
+++ b/media/libstagefright/MPEG4Writer.cpp
@@ -1430,9 +1430,6 @@
     int64_t previousPausedDurationUs = 0;
     int64_t timestampUs;
 
-    int64_t wallClockTimeUs = 0;
-    int64_t lastWallClockTimeUs = 0;
-
     sp<MetaData> meta_data;
     bool collectStats = collectStatisticalData();
 
@@ -1542,14 +1539,15 @@
             // of neighboring samples. This in turn helps reduce the track header size,
             // especially, the number of entries in the "stts" box.
             if (mNumSamples > 1) {
-                int64_t durationUs = timestampUs + mOwner->getDriftTimeUs() - lastTimestampUs;
+                int64_t currDriftTimeUs = mOwner->getDriftTimeUs();
+                int64_t durationUs = timestampUs + currDriftTimeUs - lastTimestampUs;
                 int64_t diffUs = (durationUs > lastDurationUs)
                             ? durationUs - lastDurationUs
                             : lastDurationUs - durationUs;
                 if (diffUs <= 5000) {  // XXX: Magic number 5ms
                     timestampUs = lastTimestampUs + lastDurationUs;
                 } else {
-                    timestampUs += mOwner->getDriftTimeUs();
+                    timestampUs += currDriftTimeUs;
                 }
             }
         }
@@ -1557,12 +1555,6 @@
         if (mNumSamples > 1) {
             if (timestampUs <= lastTimestampUs) {
                 LOGW("Frame arrives too late!");
-#if 0
-                // Drop the late frame.
-                copy->release();
-                copy = NULL;
-                continue;
-#else
                 // Don't drop the late frame, since dropping a frame may cause
                 // problems later during playback
 
@@ -1573,7 +1565,6 @@
                 } else {
                     timestampUs = lastTimestampUs + (1000000LL + (mTimeScale >> 1)) / mTimeScale;
                 }
-#endif
             }
         }
 
@@ -1613,12 +1604,10 @@
         lastDurationTicks = currDurationTicks;
         lastTimestampUs = timestampUs;
         if (mIsRealTimeRecording && mIsAudio) {
-            wallClockTimeUs = systemTime() / 1000;
-            int64_t wallClockDurationUs = wallClockTimeUs - lastWallClockTimeUs;
-            if (mNumSamples > 2) {
-                mOwner->addDriftTimeUs(lastDurationUs - wallClockDurationUs);
+            int64_t driftTimeUs = 0;
+            if (meta_data->findInt64(kKeyDriftTime, &driftTimeUs)) {
+                mOwner->setDriftTimeUs(driftTimeUs);
             }
-            lastWallClockTimeUs = wallClockTimeUs;
         }
 
         if (isSync != 0) {
@@ -1851,10 +1840,10 @@
     }
 }
 
-void MPEG4Writer::addDriftTimeUs(int64_t driftTimeUs) {
-    LOGV("addDriftTimeUs: %lld us", driftTimeUs);
+void MPEG4Writer::setDriftTimeUs(int64_t driftTimeUs) {
+    LOGV("setDriftTimeUs: %lld us", driftTimeUs);
     Mutex::Autolock autolock(mLock);
-    mDriftTimeUs += driftTimeUs;
+    mDriftTimeUs = driftTimeUs;
 }
 
 int64_t MPEG4Writer::getDriftTimeUs() {
diff --git a/media/libstagefright/codecs/aacenc/AACEncoder.cpp b/media/libstagefright/codecs/aacenc/AACEncoder.cpp
index 052c354..c05e3e5 100644
--- a/media/libstagefright/codecs/aacenc/AACEncoder.cpp
+++ b/media/libstagefright/codecs/aacenc/AACEncoder.cpp
@@ -208,6 +208,8 @@
     MediaBuffer *buffer;
     CHECK_EQ(mBufferGroup->acquire_buffer(&buffer), OK);
     uint8_t *outPtr = (uint8_t *)buffer->data();
+    bool readFromSource = false;
+    int64_t wallClockTimeUs = 0;
 
     if (mFrameCount == 0) {
         memcpy(outPtr, mAudioSpecificConfigData, 2);
@@ -238,9 +240,14 @@
             CHECK_EQ(align, 0);
 
             int64_t timeUs;
+            CHECK(mInputBuffer->meta_data()->findInt64(kKeyDriftTime, &timeUs));
+            wallClockTimeUs = timeUs;
             if (mInputBuffer->meta_data()->findInt64(kKeyTime, &timeUs)) {
                 mAnchorTimeUs = timeUs;
             }
+            readFromSource = true;
+        } else {
+            readFromSource = false;
         }
         size_t copy =
             (kNumSamplesPerFrame - mNumInputSamples) * sizeof(int16_t);
@@ -288,9 +295,13 @@
     CHECK(outputData.Length != 0);
     buffer->set_range(0, outputData.Length);
 
-    int64_t timestampUs = ((mFrameCount - 1) * 1000000LL * kNumSamplesPerFrame) / mSampleRate;
+    int64_t mediaTimeUs =
+        ((mFrameCount - 1) * 1000000LL * kNumSamplesPerFrame) / mSampleRate;
+    buffer->meta_data()->setInt64(kKeyTime, mAnchorTimeUs + mediaTimeUs);
+    if (readFromSource) {
+        buffer->meta_data()->setInt64(kKeyDriftTime, mediaTimeUs - wallClockTimeUs);
+    }
     ++mFrameCount;
-    buffer->meta_data()->setInt64(kKeyTime, timestampUs);
 
     *out = buffer;
     return OK;
diff --git a/media/libstagefright/codecs/amrnb/enc/AMRNBEncoder.cpp b/media/libstagefright/codecs/amrnb/enc/AMRNBEncoder.cpp
index c875426..dab1390 100644
--- a/media/libstagefright/codecs/amrnb/enc/AMRNBEncoder.cpp
+++ b/media/libstagefright/codecs/amrnb/enc/AMRNBEncoder.cpp
@@ -147,6 +147,8 @@
     int64_t seekTimeUs;
     ReadOptions::SeekMode mode;
     CHECK(options == NULL || !options->getSeekTo(&seekTimeUs, &mode));
+    bool readFromSource = false;
+    int64_t wallClockTimeUs = 0;
 
     while (mNumInputSamples < kNumSamplesPerFrame) {
         if (mInputBuffer == NULL) {
@@ -166,12 +168,16 @@
 
             size_t align = mInputBuffer->range_length() % sizeof(int16_t);
             CHECK_EQ(align, 0);
+            readFromSource = true;
 
             int64_t timeUs;
+            CHECK(mInputBuffer->meta_data()->findInt64(kKeyDriftTime, &timeUs));
+            wallClockTimeUs = timeUs;
             if (mInputBuffer->meta_data()->findInt64(kKeyTime, &timeUs)) {
                 mAnchorTimeUs = timeUs;
-                mNumFramesOutput = 0;
             }
+        } else {
+            readFromSource = false;
         }
 
         size_t copy =
@@ -217,8 +223,14 @@
     buffer->set_range(0, res);
 
     // Each frame of 160 samples is 20ms long.
+    int64_t mediaTimeUs = mNumFramesOutput * 20000LL;
     buffer->meta_data()->setInt64(
-            kKeyTime, mAnchorTimeUs + mNumFramesOutput * 20000);
+            kKeyTime, mAnchorTimeUs + mediaTimeUs);
+
+    if (readFromSource) {
+        buffer->meta_data()->setInt64(kKeyDriftTime,
+            mediaTimeUs - wallClockTimeUs);
+    }
 
     ++mNumFramesOutput;
 
diff --git a/media/libstagefright/codecs/amrwbenc/AMRWBEncoder.cpp b/media/libstagefright/codecs/amrwbenc/AMRWBEncoder.cpp
index 93304d0..b62eb5b 100644
--- a/media/libstagefright/codecs/amrwbenc/AMRWBEncoder.cpp
+++ b/media/libstagefright/codecs/amrwbenc/AMRWBEncoder.cpp
@@ -198,6 +198,8 @@
     int64_t seekTimeUs;
     ReadOptions::SeekMode mode;
     CHECK(options == NULL || !options->getSeekTo(&seekTimeUs, &mode));
+    bool readFromSource = false;
+    int64_t wallClockTimeUs = 0;
 
     while (mNumInputSamples < kNumSamplesPerFrame) {
         if (mInputBuffer == NULL) {
@@ -219,9 +221,14 @@
             CHECK_EQ(align, 0);
 
             int64_t timeUs;
+            CHECK(mInputBuffer->meta_data()->findInt64(kKeyDriftTime, &timeUs));
+            wallClockTimeUs = timeUs;
             if (mInputBuffer->meta_data()->findInt64(kKeyTime, &timeUs)) {
                 mAnchorTimeUs = timeUs;
             }
+            readFromSource = true;
+        } else {
+            readFromSource = false;
         }
 
         size_t copy =
@@ -276,10 +283,11 @@
     buffer->set_range(0, outputData.Length);
     ++mNumFramesOutput;
 
-    // XXX: fix timestamp calculation
-    int64_t timestampUs = mNumFramesOutput * 20000LL;
-
-    buffer->meta_data()->setInt64(kKeyTime, timestampUs);
+    int64_t mediaTimeUs = mNumFramesOutput * 20000LL;
+    buffer->meta_data()->setInt64(kKeyTime, mAnchorTimeUs + mediaTimeUs);
+    if (readFromSource) {
+        buffer->meta_data()->setInt64(kKeyDriftTime, mediaTimeUs - wallClockTimeUs);
+    }
 
     *out = buffer;
     return OK;
diff --git a/services/audioflinger/AudioFlinger.cpp b/services/audioflinger/AudioFlinger.cpp
index 1acdaaf..3770b55 100644
--- a/services/audioflinger/AudioFlinger.cpp
+++ b/services/audioflinger/AudioFlinger.cpp
@@ -5992,12 +5992,14 @@
                                             p,
                                             &rsize,
                                             &reply);
-            if (ret == NO_ERROR) {
-                if (reply != NO_ERROR) {
-                    status = reply;
-                }
-            } else {
+            // stop at first error encountered
+            if (ret != NO_ERROR) {
                 status = ret;
+                *(int *)pReplyData = reply;
+                break;
+            } else if (reply != NO_ERROR) {
+                *(int *)pReplyData = reply;
+                break;
             }
             mCblk->serverIndex += size;
         }
@@ -6005,8 +6007,10 @@
         mCblk->clientIndex = 0;
         return status;
     } else if (cmdCode == EFFECT_CMD_ENABLE) {
+        *(int *)pReplyData = NO_ERROR;
         return enable();
     } else if (cmdCode == EFFECT_CMD_DISABLE) {
+        *(int *)pReplyData = NO_ERROR;
         return disable();
     }
 
diff --git a/tools/aapt/Command.cpp b/tools/aapt/Command.cpp
index b0f086b..f71ebb9 100644
--- a/tools/aapt/Command.cpp
+++ b/tools/aapt/Command.cpp
@@ -853,6 +853,15 @@
                                     error.string());
                             goto bail;
                         }
+                    } else if (tag == "uses-package") {
+                        String8 name = getAttribute(tree, NAME_ATTR, &error);
+                        if (name != "" && error == "") {
+                            printf("uses-package:'%s'\n", name.string());
+                        } else {
+                            fprintf(stderr, "ERROR getting 'android:name' attribute: %s\n",
+                                    error.string());
+                                goto bail;
+                        }
                     } else if (tag == "original-package") {
                         String8 name = getAttribute(tree, NAME_ATTR, &error);
                         if (name != "" && error == "") {