Merge "Calculate audio media drift time from AudioSource" into gingerbread
diff --git a/api/9.xml b/api/9.xml
index 46f087f..b266352 100644
--- a/api/9.xml
+++ b/api/9.xml
@@ -793,17 +793,6 @@
  visibility="public"
 >
 </field>
-<field name="READ_OWNER_DATA"
- type="java.lang.String"
- transient="false"
- volatile="false"
- value="&quot;android.permission.READ_OWNER_DATA&quot;"
- static="true"
- final="true"
- deprecated="not deprecated"
- visibility="public"
->
-</field>
 <field name="READ_PHONE_STATE"
  type="java.lang.String"
  transient="false"
@@ -1233,17 +1222,6 @@
  visibility="public"
 >
 </field>
-<field name="WRITE_OWNER_DATA"
- type="java.lang.String"
- transient="false"
- volatile="false"
- value="&quot;android.permission.WRITE_OWNER_DATA&quot;"
- static="true"
- final="true"
- deprecated="not deprecated"
- visibility="public"
->
-</field>
 <field name="WRITE_SECURE_SETTINGS"
  type="java.lang.String"
  transient="false"
diff --git a/api/current.xml b/api/current.xml
index 3c08549..f7a5954 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -712,7 +712,7 @@
  value="&quot;android.permission.PERSISTENT_ACTIVITY&quot;"
  static="true"
  final="true"
- deprecated="not deprecated"
+ deprecated="deprecated"
  visibility="public"
 >
 </field>
@@ -793,17 +793,6 @@
  visibility="public"
 >
 </field>
-<field name="READ_OWNER_DATA"
- type="java.lang.String"
- transient="false"
- volatile="false"
- value="&quot;android.permission.READ_OWNER_DATA&quot;"
- static="true"
- final="true"
- deprecated="not deprecated"
- visibility="public"
->
-</field>
 <field name="READ_PHONE_STATE"
  type="java.lang.String"
  transient="false"
@@ -1233,17 +1222,6 @@
  visibility="public"
 >
 </field>
-<field name="WRITE_OWNER_DATA"
- type="java.lang.String"
- transient="false"
- volatile="false"
- value="&quot;android.permission.WRITE_OWNER_DATA&quot;"
- static="true"
- final="true"
- deprecated="not deprecated"
- visibility="public"
->
-</field>
 <field name="WRITE_SECURE_SETTINGS"
  type="java.lang.String"
  transient="false"
@@ -20516,7 +20494,7 @@
  synchronized="false"
  static="false"
  final="false"
- deprecated="not deprecated"
+ deprecated="deprecated"
  visibility="public"
 >
 <parameter name="isPersistent" type="boolean">
@@ -203586,7 +203564,7 @@
  synchronized="true"
  static="false"
  final="false"
- deprecated="not deprecated"
+ deprecated="deprecated"
  visibility="public"
 >
 </method>
@@ -204134,7 +204112,7 @@
  synchronized="true"
  static="false"
  final="false"
- deprecated="not deprecated"
+ deprecated="deprecated"
  visibility="public"
 >
 <parameter name="pluginsPath" type="java.lang.String">
@@ -252670,7 +252648,7 @@
  deprecated="not deprecated"
  visibility="public"
 >
-<parameter name="lng" type="long">
+<parameter name="l" type="long">
 </parameter>
 </method>
 <method name="append"
@@ -252748,7 +252726,7 @@
  deprecated="not deprecated"
  visibility="public"
 >
-<parameter name="ch" type="char[]">
+<parameter name="chars" type="char[]">
 </parameter>
 </method>
 <method name="append"
@@ -307319,9 +307297,9 @@
 >
 <parameter name="number" type="java.lang.Object">
 </parameter>
-<parameter name="toAppendTo" type="java.lang.StringBuffer">
+<parameter name="buffer" type="java.lang.StringBuffer">
 </parameter>
-<parameter name="pos" type="java.text.FieldPosition">
+<parameter name="position" type="java.text.FieldPosition">
 </parameter>
 </method>
 <method name="getDecimalFormatSymbols"
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index f7a9a18..6e6e86f 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1620,6 +1620,9 @@
     }
 
     /**
+     * @deprecated This functionality will be removed in the future; please do
+     * not use.
+     *
      * Control whether this activity is required to be persistent.  By default
      * activities are not persistent; setting this to true will prevent the
      * system from stopping this activity or its process when running low on
@@ -1634,6 +1637,7 @@
      *                     persistent, true if so, false for the normal
      *                     behavior.
      */
+    @Deprecated
     public void setPersistent(boolean isPersistent) {
         if (mParent == null) {
             try {
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/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index aa9fe76..4d2ba71 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -143,12 +143,12 @@
     /**
      * Max distance to overscroll for edge effects
      */
-    private static final int OVERSCROLL_DISTANCE = 4;
+    private static final int OVERSCROLL_DISTANCE = 2;
 
     /**
      * Max distance to overfling for edge effects
      */
-    private static final int OVERFLING_DISTANCE = 8;
+    private static final int OVERFLING_DISTANCE = 4;
 
     private final int mEdgeSlop;
     private final int mFadingEdgeLength;
diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java
index 4e57a8a..da0c61b 100644
--- a/core/java/android/webkit/WebSettings.java
+++ b/core/java/android/webkit/WebSettings.java
@@ -1054,6 +1054,7 @@
      * @deprecated This method is no longer used as plugins are loaded from
      * their own APK via the system's package manager.
      */
+    @Deprecated
     public synchronized void setPluginsPath(String pluginsPath) {
     }
 
@@ -1229,6 +1230,7 @@
      * @deprecated This method is no longer used as plugins are loaded from
      * their own APK via the system's package manager.
      */
+    @Deprecated
     public synchronized String getPluginsPath() {
         return "";
     }
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index 7898083..c01068f 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -1041,7 +1041,7 @@
         if (mode != OVERSCROLL_NEVER) {
             if (mEdgeGlowTop == null) {
                 final Resources res = getContext().getResources();
-                final Drawable edge = res.getDrawable(R.drawable.edge_light);
+                final Drawable edge = res.getDrawable(R.drawable.overscroll_edge);
                 final Drawable glow = res.getDrawable(R.drawable.overscroll_glow);
                 mEdgeGlowTop = new EdgeGlow(edge, glow);
                 mEdgeGlowBottom = new EdgeGlow(edge, glow);
@@ -2567,11 +2567,6 @@
             mInOverScrollMode = true;
         }
 
-        if ((clampedX && maxX > 0) || clampedY) {
-            // Hitting a scroll barrier breaks velocity; don't fling further.
-            mVelocityTracker.clear();
-            mLastVelocity = 0;
-        }
         super.scrollTo(scrollX, scrollY);
     }
 
@@ -3469,8 +3464,8 @@
     }
 
     @Override
-    protected void dispatchDraw(Canvas canvas) {
-        super.dispatchDraw(canvas);
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
         if (mEdgeGlowTop != null && drawEdgeGlows(canvas)) {
             invalidate();
         }
@@ -3492,7 +3487,7 @@
         if (!mEdgeGlowTop.isFinished()) {
             final int restoreCount = canvas.save();
 
-            canvas.translate(-width / 2 + scrollX, scrollY);
+            canvas.translate(-width / 2 + scrollX, Math.min(0, scrollY));
             mEdgeGlowTop.setSize(width * 2, height);
             invalidateForGlow |= mEdgeGlowTop.draw(canvas);
             canvas.restoreToCount(restoreCount);
@@ -3500,7 +3495,7 @@
         if (!mEdgeGlowBottom.isFinished()) {
             final int restoreCount = canvas.save();
 
-            canvas.translate(-width / 2 - scrollX, scrollY + height);
+            canvas.translate(-width / 2 + scrollX, Math.max(computeMaxScrollY(), scrollY) + height);
             canvas.rotate(180, width, 0);
             mEdgeGlowBottom.setSize(width * 2, height);
             invalidateForGlow |= mEdgeGlowBottom.draw(canvas);
@@ -3510,7 +3505,7 @@
             final int restoreCount = canvas.save();
 
             canvas.rotate(270);
-            canvas.translate(-height * 1.5f - scrollY, scrollX);
+            canvas.translate(-height * 1.5f - scrollY, Math.min(0, scrollX));
             mEdgeGlowLeft.setSize(height * 2, width);
             invalidateForGlow |= mEdgeGlowLeft.draw(canvas);
             canvas.restoreToCount(restoreCount);
@@ -3519,7 +3514,8 @@
             final int restoreCount = canvas.save();
 
             canvas.rotate(90);
-            canvas.translate(-height / 2 + scrollY, -scrollX - width);
+            canvas.translate(-height / 2 + scrollY,
+                    -(Math.max(computeMaxScrollX(), scrollX) + width));
             mEdgeGlowRight.setSize(height * 2, width);
             invalidateForGlow |= mEdgeGlowRight.draw(canvas);
             canvas.restoreToCount(restoreCount);
@@ -5977,6 +5973,24 @@
                     + " maxX=" + maxX + " maxY=" + maxY
                     + " mScrollX=" + mScrollX + " mScrollY=" + mScrollY);
         }
+
+        // Allow sloppy flings without overscrolling at the edges.
+        if ((mScrollX == 0 || mScrollX == maxX) && Math.abs(vx) < Math.abs(vy)) {
+            vx = 0;
+        }
+        if ((mScrollY == 0 || mScrollY == maxY) && Math.abs(vy) < Math.abs(vx)) {
+            vy = 0;
+        }
+
+        if (mOverscrollDistance < mOverflingDistance) {
+            if (mScrollX == -mOverscrollDistance || mScrollX == maxX + mOverscrollDistance) {
+                vx = 0;
+            }
+            if (mScrollY == -mOverscrollDistance || mScrollY == maxY + mOverscrollDistance) {
+                vy = 0;
+            }
+        }
+
         mLastVelX = vx;
         mLastVelY = vy;
         mLastVelocity = (float) Math.hypot(vx, vy);
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index db50ca1..fe2a43b 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -507,6 +507,20 @@
     private EdgeGlow mEdgeGlowBottom;
 
     /**
+     * An estimate of how many pixels are between the top of the list and
+     * the top of the first position in the adapter, based on the last time
+     * we saw it. Used to hint where to draw edge glows.
+     */
+    private int mFirstPositionDistanceGuess;
+
+    /**
+     * An estimate of how many pixels are between the bottom of the list and
+     * the bottom of the last position in the adapter, based on the last time
+     * we saw it. Used to hint where to draw edge glows.
+     */
+    private int mLastPositionDistanceGuess;
+
+    /**
      * Interface definition for a callback to be invoked when the list or grid
      * has been scrolled.
      */
@@ -632,7 +646,7 @@
         if (mode != OVERSCROLL_NEVER) {
             if (mEdgeGlowTop == null) {
                 final Resources res = getContext().getResources();
-                final Drawable edge = res.getDrawable(R.drawable.edge_light);
+                final Drawable edge = res.getDrawable(R.drawable.overscroll_edge);
                 final Drawable glow = res.getDrawable(R.drawable.overscroll_glow);
                 mEdgeGlowTop = new EdgeGlow(edge, glow);
                 mEdgeGlowBottom = new EdgeGlow(edge, glow);
@@ -1684,6 +1698,7 @@
                 mFlingRunnable.endFling();
                 if (mScrollY != 0) {
                     mScrollY = 0;
+                    finishGlows();
                     invalidate();
                 }
             }
@@ -2041,6 +2056,7 @@
 
                 if (mScrollY != 0) {
                     mScrollY = 0;
+                    finishGlows();
                     invalidate();
                 }
             }
@@ -2518,7 +2534,7 @@
                 final int restoreCount = canvas.save();
                 final int width = getWidth();
 
-                canvas.translate(-width / 2, scrollY);
+                canvas.translate(-width / 2, Math.min(0, scrollY + mFirstPositionDistanceGuess));
                 mEdgeGlowTop.setSize(width * 2, getHeight());
                 if (mEdgeGlowTop.draw(canvas)) {
                     invalidate();
@@ -2530,7 +2546,8 @@
                 final int width = getWidth();
                 final int height = getHeight();
 
-                canvas.translate(-width / 2, scrollY + height);
+                canvas.translate(-width / 2,
+                        Math.max(height, scrollY + mLastPositionDistanceGuess));
                 canvas.rotate(180, width, 0);
                 mEdgeGlowBottom.setSize(width * 2, height);
                 if (mEdgeGlowBottom.draw(canvas)) {
@@ -3223,6 +3240,18 @@
 
         final int firstPosition = mFirstPosition;
 
+        // Update our guesses for where the first and last views are
+        if (firstPosition == 0) {
+            mFirstPositionDistanceGuess = firstTop - mListPadding.top;
+        } else {
+            mFirstPositionDistanceGuess += incrementalDeltaY;
+        }
+        if (firstPosition + childCount == mItemCount) {
+            mLastPositionDistanceGuess = lastBottom + mListPadding.bottom;
+        } else {
+            mLastPositionDistanceGuess += incrementalDeltaY;
+        }
+
         if (firstPosition == 0 && firstTop >= listPadding.top && incrementalDeltaY >= 0) {
             // Don't need to move views down if the top of the first position
             // is already visible
@@ -4167,6 +4196,13 @@
         return result;
     }
 
+    private void finishGlows() {
+        if (mEdgeGlowTop != null) {
+            mEdgeGlowTop.finish();
+            mEdgeGlowBottom.finish();
+        }
+    }
+
     /**
      * Sets the recycler listener to be notified whenever a View is set aside in
      * the recycler for later reuse. This listener can be used to free resources
diff --git a/core/java/android/widget/EdgeGlow.java b/core/java/android/widget/EdgeGlow.java
index 06334a0..93222e1 100644
--- a/core/java/android/widget/EdgeGlow.java
+++ b/core/java/android/widget/EdgeGlow.java
@@ -18,7 +18,6 @@
 
 import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
-import android.util.Log;
 import android.view.animation.AnimationUtils;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
@@ -30,8 +29,6 @@
 public class EdgeGlow {
     private static final String TAG = "EdgeGlow";
 
-    private static final boolean DEBUG = false;
-
     // Time it will take the effect to fully recede in ms
     private static final int RECEDE_TIME = 1000;
 
@@ -46,7 +43,10 @@
     private static final float HELD_GLOW_ALPHA = 0.5f;
     private static final float HELD_GLOW_SCALE_Y = 0.5f;
 
+    private static final float MAX_GLOW_HEIGHT = 0.33f;
+
     private static final float PULL_GLOW_BEGIN = 0.5f;
+    private static final float PULL_EDGE_BEGIN = 0.6f;
 
     // Minimum velocity that will be absorbed
     private static final int MIN_VELOCITY = 750;
@@ -103,6 +103,10 @@
         return mState == STATE_IDLE;
     }
 
+    public void finish() {
+        mState = STATE_IDLE;
+    }
+
     /**
      * Call when the object is pulled by the user.
      * @param deltaDistance Change in distance since the last call
@@ -123,7 +127,7 @@
         mPullDistance += deltaDistance;
         float distance = Math.abs(mPullDistance);
 
-        mEdgeAlpha = mEdgeAlphaStart = Math.max(HELD_EDGE_ALPHA, Math.min(distance, 1.f));
+        mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, 1.f));
         mEdgeScaleY = mEdgeScaleYStart = Math.max(HELD_EDGE_SCALE_Y, Math.min(distance, 2.f));
 
         mGlowAlpha = mGlowAlphaStart = Math.max(0.5f,
@@ -142,8 +146,6 @@
         mEdgeScaleYFinish = mEdgeScaleY;
         mGlowAlphaFinish = mGlowAlpha;
         mGlowScaleYFinish = mGlowScaleY;
-
-        if (DEBUG) Log.d(TAG, "onPull(" + distance + ", " + deltaDistance + ")");
     }
 
     /**
@@ -155,7 +157,6 @@
         if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
             return;
         }
-        if (DEBUG) Log.d(TAG, "onRelease");
 
         mState = STATE_RECEDE;
         mEdgeAlphaStart = mEdgeAlpha;
@@ -178,7 +179,6 @@
      */
     public void onAbsorb(int velocity) {
         mState = STATE_ABSORB;
-        if (DEBUG) Log.d(TAG, "onAbsorb uncooked velocity: " + velocity);
         velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
 
         mStartTime = AnimationUtils.currentAnimationTimeMillis();
@@ -193,8 +193,6 @@
         mEdgeScaleYFinish = 1.f;
         mGlowAlphaFinish = 1.f;
         mGlowScaleYFinish = Math.min(velocity * 0.001f, 1);
-
-        if (DEBUG) Log.d(TAG, "onAbsorb(" + velocity + "): duration " + mDuration);
     }
 
     /**
@@ -212,8 +210,11 @@
         final int edgeHeight = mEdge.getIntrinsicHeight();
         final int glowHeight = mGlow.getIntrinsicHeight();
 
+        final float distScale = (float) mHeight / mWidth;
+
         mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
-        mGlow.setBounds(0, 0, mWidth, (int) (glowHeight * mGlowScaleY * 0.5f));
+        mGlow.setBounds(0, 0, mWidth, (int) Math.min(glowHeight * mGlowScaleY * distScale * 0.6f,
+                mHeight * MAX_GLOW_HEIGHT));
         mGlow.draw(canvas);
 
         mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
@@ -222,8 +223,6 @@
                 mWidth,
                 (int) (edgeHeight * mEdgeScaleY));
         mEdge.draw(canvas);
-        if (DEBUG) Log.d(TAG, "draw() glow(" + mGlowAlpha + ", " + mGlowScaleY + ") edge(" + mEdgeAlpha +
-                ", " + mEdgeScaleY + ")");
 
         return mState != STATE_IDLE;
     }
@@ -255,7 +254,6 @@
                     mEdgeScaleYFinish = 0.1f;
                     mGlowAlphaFinish = 0.f;
                     mGlowScaleYFinish = mGlowScaleY;
-                    if (DEBUG) Log.d(TAG, "STATE_ABSORB => STATE_RECEDE");
                     break;
                 case STATE_PULL:
                     mState = STATE_PULL_DECAY;
@@ -271,14 +269,12 @@
                     mEdgeScaleYFinish = Math.min(mEdgeScaleYStart, HELD_EDGE_SCALE_Y);
                     mGlowAlphaFinish = Math.min(mGlowAlphaStart, HELD_GLOW_ALPHA);
                     mGlowScaleYFinish = Math.min(mGlowScaleY, HELD_GLOW_SCALE_Y);
-                    if (DEBUG) Log.d(TAG, "STATE_PULL => STATE_PULL_DECAY");
                     break;
                 case STATE_PULL_DECAY:
                     // Do nothing; wait for release
                     break;
                 case STATE_RECEDE:
                     mState = STATE_IDLE;
-                    if (DEBUG) Log.d(TAG, "STATE_RECEDE => STATE_IDLE");
                     break;
             }
         }
diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java
index 3493f49..129ad8a 100644
--- a/core/java/android/widget/HorizontalScrollView.java
+++ b/core/java/android/widget/HorizontalScrollView.java
@@ -1374,7 +1374,7 @@
         if (mode != OVERSCROLL_NEVER) {
             if (mEdgeGlowLeft == null) {
                 final Resources res = getContext().getResources();
-                final Drawable edge = res.getDrawable(R.drawable.edge_light);
+                final Drawable edge = res.getDrawable(R.drawable.overscroll_edge);
                 final Drawable glow = res.getDrawable(R.drawable.overscroll_glow);
                 mEdgeGlowLeft = new EdgeGlow(edge, glow);
                 mEdgeGlowRight = new EdgeGlow(edge, glow);
@@ -1396,7 +1396,7 @@
                 final int height = getHeight();
 
                 canvas.rotate(270);
-                canvas.translate(-height * 1.5f, scrollX);
+                canvas.translate(-height * 1.5f, Math.min(0, scrollX));
                 mEdgeGlowLeft.setSize(getHeight() * 2, getWidth());
                 if (mEdgeGlowLeft.draw(canvas)) {
                     invalidate();
@@ -1409,7 +1409,7 @@
                 final int height = getHeight();
 
                 canvas.rotate(90);
-                canvas.translate(-height / 2, -scrollX - width);
+                canvas.translate(-height / 2, -(Math.max(getScrollRange(), scrollX) + width));
                 mEdgeGlowRight.setSize(height * 2, width);
                 if (mEdgeGlowRight.draw(canvas)) {
                     invalidate();
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index 9d971f6..7b5e412 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -1374,7 +1374,7 @@
         if (mode != OVERSCROLL_NEVER) {
             if (mEdgeGlowTop == null) {
                 final Resources res = getContext().getResources();
-                final Drawable edge = res.getDrawable(R.drawable.edge_light);
+                final Drawable edge = res.getDrawable(R.drawable.overscroll_edge);
                 final Drawable glow = res.getDrawable(R.drawable.overscroll_glow);
                 mEdgeGlowTop = new EdgeGlow(edge, glow);
                 mEdgeGlowBottom = new EdgeGlow(edge, glow);
@@ -1395,7 +1395,7 @@
                 final int restoreCount = canvas.save();
                 final int width = getWidth();
 
-                canvas.translate(-width / 2, scrollY);
+                canvas.translate(-width / 2, Math.min(0, scrollY));
                 mEdgeGlowTop.setSize(width * 2, getHeight());
                 if (mEdgeGlowTop.draw(canvas)) {
                     invalidate();
@@ -1407,7 +1407,7 @@
                 final int width = getWidth();
                 final int height = getHeight();
 
-                canvas.translate(-width / 2, scrollY + height);
+                canvas.translate(-width / 2, Math.max(getScrollRange(), scrollY) + height);
                 canvas.rotate(180, width, 0);
                 mEdgeGlowBottom.setSize(width * 2, height);
                 if (mEdgeGlowBottom.draw(canvas)) {
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 62a4495..2751a82 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -308,6 +308,8 @@
     jclass clazz;
     jmethodID methodId;
 
+    LOGD("Calling main entry %s", className);
+
     env = getJNIEnv();
     if (env == NULL)
         return UNKNOWN_ERROR;
@@ -914,7 +916,8 @@
  */
 void AndroidRuntime::start(const char* className, const bool startSystemServer)
 {
-    LOGD("\n>>>>>>>>>>>>>> AndroidRuntime START <<<<<<<<<<<<<<\n");
+    LOGD("\n>>>>>> AndroidRuntime START %s <<<<<<\n",
+            className != NULL ? className : "(unknown)");
 
     char* slashClassName = NULL;
     char* cp;
@@ -1029,7 +1032,7 @@
 
 void AndroidRuntime::onExit(int code)
 {
-    LOGI("AndroidRuntime onExit calling exit(%d)", code);
+    LOGV("AndroidRuntime onExit calling exit(%d)", code);
     exit(code);
 }
 
@@ -1334,7 +1337,7 @@
      */
     androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
 
-    LOGD("--- registering native functions ---\n");
+    LOGV("--- registering native functions ---\n");
 
     /*
      * Every "register" function calls one or more things that return
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 19b30cc..0323b70 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -191,20 +191,6 @@
         android:label="@string/permlab_writeContacts"
         android:description="@string/permdesc_writeContacts" />
 
-    <!-- Allows an application to read the owner's data. -->
-    <permission android:name="android.permission.READ_OWNER_DATA"
-        android:permissionGroup="android.permission-group.PERSONAL_INFO"
-        android:protectionLevel="dangerous"
-        android:label="@string/permlab_readOwnerData"
-        android:description="@string/permdesc_readOwnerData" />
-
-    <!-- Allows an application to write (but not read) the owner's data. -->
-    <permission android:name="android.permission.WRITE_OWNER_DATA"
-        android:permissionGroup="android.permission-group.PERSONAL_INFO"
-        android:protectionLevel="dangerous"
-        android:label="@string/permlab_writeOwnerData"
-        android:description="@string/permdesc_writeOwnerData" />
-
     <!-- Allows an application to read the user's calendar data. -->
     <permission android:name="android.permission.READ_CALENDAR"
         android:permissionGroup="android.permission-group.PERSONAL_INFO"
@@ -625,7 +611,10 @@
         android:label="@string/permlab_setAnimationScale"
         android:description="@string/permdesc_setAnimationScale" />
 
-    <!-- Allow an application to make its activities persistent. -->
+    <!-- @deprecated This functionality will be removed in the future; please do
+         not use.
+
+         Allow an application to make its activities persistent. -->
     <permission android:name="android.permission.PERSISTENT_ACTIVITY"
         android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
         android:protectionLevel="dangerous"
@@ -767,7 +756,7 @@
     <!-- Allows applications to disable the keyguard -->
     <permission android:name="android.permission.DISABLE_KEYGUARD"
         android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
-        android:protectionLevel="normal"
+        android:protectionLevel="dangerous"
         android:description="@string/permdesc_disableKeyguard"
         android:label="@string/permlab_disableKeyguard" />
 
diff --git a/core/res/res/drawable/edge_light.png b/core/res/res/drawable/edge_light.png
deleted file mode 100644
index b026880..0000000
--- a/core/res/res/drawable/edge_light.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable/overscroll_edge.png b/core/res/res/drawable/overscroll_edge.png
new file mode 100644
index 0000000..250c827
--- /dev/null
+++ b/core/res/res/drawable/overscroll_edge.png
Binary files differ
diff --git a/core/res/res/drawable/overscroll_glow.png b/core/res/res/drawable/overscroll_glow.png
index 7f1831e..69b456d 100644
--- a/core/res/res/drawable/overscroll_glow.png
+++ b/core/res/res/drawable/overscroll_glow.png
Binary files differ
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 03b721c..29ac2ea 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -784,20 +784,6 @@
         applications can use this to erase or modify your contact data.</string>
 
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
-    <string name="permlab_writeOwnerData">write owner data</string>
-    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
-    <string name="permdesc_writeOwnerData">Allows an application to modify the
-        phone owner data stored on your phone. Malicious
-        applications can use this to erase or modify owner data.</string>
-
-    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
-    <string name="permlab_readOwnerData">read owner data</string>
-    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
-    <string name="permdesc_readOwnerData">Allows an application read the
-        phone owner data stored on your phone. Malicious
-        applications can use this to read phone owner data.</string>
-
-    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_readCalendar">read calendar events</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_readCalendar">Allows an application to read all
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 cef15fd7..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/data/etc/platform.xml b/data/etc/platform.xml
index 7322e6c..6f8afff 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -90,8 +90,6 @@
     <assign-permission name="android.permission.CALL_PHONE" uid="shell" />
     <assign-permission name="android.permission.READ_CONTACTS" uid="shell" />
     <assign-permission name="android.permission.WRITE_CONTACTS" uid="shell" />
-    <assign-permission name="android.permission.READ_OWNER_DATA" uid="shell" />
-    <assign-permission name="android.permission.WRITE_OWNER_DATA" uid="shell" />
     <assign-permission name="android.permission.READ_CALENDAR" uid="shell" />
     <assign-permission name="android.permission.WRITE_CALENDAR" uid="shell" />
     <assign-permission name="android.permission.READ_USER_DICTIONARY" uid="shell" />
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/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/services/java/com/android/server/am/ActivityStack.java b/services/java/com/android/server/am/ActivityStack.java
index c21a221..5f79b85 100644
--- a/services/java/com/android/server/am/ActivityStack.java
+++ b/services/java/com/android/server/am/ActivityStack.java
@@ -2885,7 +2885,8 @@
                     + " res=" + resultCode + " data=" + resultData);
             if (r.info.applicationInfo.uid > 0) {
                 mService.grantUriPermissionFromIntentLocked(r.info.applicationInfo.uid,
-                        r.packageName, resultData, r.getUriPermissionsLocked());
+                        resultTo.packageName, resultData, 
+                        resultTo.getUriPermissionsLocked());
             }
             resultTo.addResultLocked(r, r.resultWho, r.requestCode, resultCode,
                                      resultData);
diff --git a/services/java/com/android/server/sip/SipHelper.java b/services/java/com/android/server/sip/SipHelper.java
index 83eeb84..d9a1bbf 100644
--- a/services/java/com/android/server/sip/SipHelper.java
+++ b/services/java/com/android/server/sip/SipHelper.java
@@ -20,7 +20,6 @@
 import gov.nist.javax.sip.clientauthutils.AccountManager;
 import gov.nist.javax.sip.clientauthutils.AuthenticationHelper;
 
-import android.net.sip.SessionDescription;
 import android.net.sip.SipProfile;
 import android.util.Log;
 
@@ -243,7 +242,7 @@
     }
 
     public ClientTransaction sendInvite(SipProfile caller, SipProfile callee,
-            SessionDescription sessionDescription, String tag)
+            String sessionDescription, String tag)
             throws SipException {
         try {
             FromHeader fromHeader = createFromHeader(caller, tag);
@@ -259,9 +258,9 @@
                     toHeader, viaHeaders, maxForwards);
 
             request.addHeader(createContactHeader(caller));
-            request.setContent(sessionDescription.getContent(),
+            request.setContent(sessionDescription,
                     mHeaderFactory.createContentTypeHeader(
-                            "application", sessionDescription.getType()));
+                            "application", "sdp"));
 
             ClientTransaction clientTransaction =
                     mSipProvider.getNewClientTransaction(request);
@@ -273,12 +272,12 @@
     }
 
     public ClientTransaction sendReinvite(Dialog dialog,
-            SessionDescription sessionDescription) throws SipException {
+            String sessionDescription) throws SipException {
         try {
             Request request = dialog.createRequest(Request.INVITE);
-            request.setContent(sessionDescription.getContent(),
+            request.setContent(sessionDescription,
                     mHeaderFactory.createContentTypeHeader(
-                            "application", sessionDescription.getType()));
+                            "application", "sdp"));
 
             ClientTransaction clientTransaction =
                     mSipProvider.getNewClientTransaction(request);
@@ -326,7 +325,7 @@
      * @param event the INVITE request event
      */
     public ServerTransaction sendInviteOk(RequestEvent event,
-            SipProfile localProfile, SessionDescription sessionDescription,
+            SipProfile localProfile, String sessionDescription,
             ServerTransaction inviteTransaction)
             throws SipException {
         try {
@@ -334,9 +333,9 @@
             Response response = mMessageFactory.createResponse(Response.OK,
                     request);
             response.addHeader(createContactHeader(localProfile));
-            response.setContent(sessionDescription.getContent(),
+            response.setContent(sessionDescription,
                     mHeaderFactory.createContentTypeHeader(
-                            "application", sessionDescription.getType()));
+                            "application", "sdp"));
 
             if (inviteTransaction == null) {
                 inviteTransaction = getServerTransaction(event);
diff --git a/services/java/com/android/server/sip/SipService.java b/services/java/com/android/server/sip/SipService.java
index 626b488..563ce58 100644
--- a/services/java/com/android/server/sip/SipService.java
+++ b/services/java/com/android/server/sip/SipService.java
@@ -53,6 +53,9 @@
 import java.util.TreeSet;
 import javax.sip.SipException;
 
+/**
+ * @hide
+ */
 public final class SipService extends ISipService.Stub {
     private static final String TAG = "SipService";
     private static final int EXPIRY_TIME = 3600;
@@ -442,7 +445,7 @@
 
         @Override
         public void onRinging(ISipSession session, SipProfile caller,
-                byte[] sessionDescription) {
+                String sessionDescription) {
             synchronized (SipService.this) {
                 try {
                     if (!isRegistered()) {
diff --git a/services/java/com/android/server/sip/SipSessionGroup.java b/services/java/com/android/server/sip/SipSessionGroup.java
index d33558b..8019bfa 100644
--- a/services/java/com/android/server/sip/SipSessionGroup.java
+++ b/services/java/com/android/server/sip/SipSessionGroup.java
@@ -20,6 +20,7 @@
 import gov.nist.javax.sip.clientauthutils.UserCredentials;
 import gov.nist.javax.sip.header.SIPHeaderNames;
 import gov.nist.javax.sip.header.WWWAuthenticate;
+import gov.nist.javax.sip.message.SIPMessage;
 
 import android.net.sip.ISipSession;
 import android.net.sip.ISipSessionListener;
@@ -31,6 +32,7 @@
 import android.util.Log;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.net.DatagramSocket;
 import java.text.ParseException;
 import java.util.Collection;
@@ -284,6 +286,22 @@
         }
     }
 
+    private String extractContent(Message message) {
+        // Currently we do not support secure MIME bodies.
+        byte[] bytes = message.getRawContent();
+        if (bytes != null) {
+            try {
+                if (message instanceof SIPMessage) {
+                    return ((SIPMessage) message).getMessageContent();
+                } else {
+                    return new String(bytes, "UTF-8");
+                }
+            } catch (UnsupportedEncodingException e) {
+            }
+        }
+        return null;
+    }
+
     private class SipSessionCallReceiverImpl extends SipSessionImpl {
         public SipSessionCallReceiverImpl(ISipSessionListener listener) {
             super(listener);
@@ -302,7 +320,7 @@
                 newSession.mPeerProfile = createPeerProfile(event.getRequest());
                 newSession.mState = SipSessionState.INCOMING_CALL;
                 newSession.mPeerSessionDescription =
-                        event.getRequest().getRawContent();
+                        extractContent(event.getRequest());
                 addSipSession(newSession);
                 mProxy.onRinging(newSession, newSession.mPeerProfile,
                         newSession.mPeerSessionDescription);
@@ -321,7 +339,7 @@
         Dialog mDialog;
         ServerTransaction mServerTransaction;
         ClientTransaction mClientTransaction;
-        byte[] mPeerSessionDescription;
+        String mPeerSessionDescription;
         boolean mInCall;
         boolean mReRegisterFlag = false;
 
@@ -401,12 +419,12 @@
         }
 
         public void makeCall(SipProfile peerProfile,
-                SessionDescription sessionDescription) {
+                String sessionDescription) {
             doCommandAsync(
                     new MakeCallCommand(peerProfile, sessionDescription));
         }
 
-        public void answerCall(SessionDescription sessionDescription) {
+        public void answerCall(String sessionDescription) {
             try {
                 processCommand(
                         new MakeCallCommand(mPeerProfile, sessionDescription));
@@ -419,7 +437,7 @@
             doCommandAsync(END_CALL);
         }
 
-        public void changeCall(SessionDescription sessionDescription) {
+        public void changeCall(String sessionDescription) {
             doCommandAsync(
                     new MakeCallCommand(mPeerProfile, sessionDescription));
         }
@@ -726,10 +744,9 @@
             if (evt instanceof MakeCallCommand) {
                 MakeCallCommand cmd = (MakeCallCommand) evt;
                 mPeerProfile = cmd.getPeerProfile();
-                SessionDescription sessionDescription =
-                        cmd.getSessionDescription();
                 mClientTransaction = mSipHelper.sendInvite(mLocalProfile,
-                        mPeerProfile, sessionDescription, generateTag());
+                        mPeerProfile, cmd.getSessionDescription(),
+                        generateTag());
                 mDialog = mClientTransaction.getDialog();
                 addSipSession(this);
                 mState = SipSessionState.OUTGOING_CALL;
@@ -811,7 +828,7 @@
                     return true;
                 case Response.OK:
                     mSipHelper.sendInviteAck(event, mDialog);
-                    mPeerSessionDescription = response.getRawContent();
+                    mPeerSessionDescription = extractContent(response);
                     establishCall();
                     return true;
                 case Response.PROXY_AUTHENTICATION_REQUIRED:
@@ -897,7 +914,7 @@
                 // got Re-INVITE
                 RequestEvent event = mInviteReceived = (RequestEvent) evt;
                 mState = SipSessionState.INCOMING_CALL;
-                mPeerSessionDescription = event.getRequest().getRawContent();
+                mPeerSessionDescription = extractContent(event.getRequest());
                 mServerTransaction = null;
                 mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription);
                 return true;
@@ -1060,10 +1077,10 @@
     }
 
     private class MakeCallCommand extends EventObject {
-        private SessionDescription mSessionDescription;
+        private String mSessionDescription;
 
         public MakeCallCommand(SipProfile peerProfile,
-                SessionDescription sessionDescription) {
+                String sessionDescription) {
             super(peerProfile);
             mSessionDescription = sessionDescription;
         }
@@ -1072,7 +1089,7 @@
             return (SipProfile) getSource();
         }
 
-        public SessionDescription getSessionDescription() {
+        public String getSessionDescription() {
             return mSessionDescription;
         }
     }
diff --git a/services/java/com/android/server/sip/SipSessionListenerProxy.java b/services/java/com/android/server/sip/SipSessionListenerProxy.java
index fd49fd8..7004204 100644
--- a/services/java/com/android/server/sip/SipSessionListenerProxy.java
+++ b/services/java/com/android/server/sip/SipSessionListenerProxy.java
@@ -56,7 +56,7 @@
     }
 
     public void onRinging(final ISipSession session, final SipProfile caller,
-            final byte[] sessionDescription) {
+            final String sessionDescription) {
         if (mListener == null) return;
         proxy(new Runnable() {
             public void run() {
@@ -83,7 +83,7 @@
     }
 
     public void onCallEstablished(final ISipSession session,
-            final byte[] sessionDescription) {
+            final String sessionDescription) {
         if (mListener == null) return;
         proxy(new Runnable() {
             public void run() {
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 == "") {
diff --git a/voip/java/android/net/sip/ISipSession.aidl b/voip/java/android/net/sip/ISipSession.aidl
index fbcb056..1a23527 100644
--- a/voip/java/android/net/sip/ISipSession.aidl
+++ b/voip/java/android/net/sip/ISipSession.aidl
@@ -115,8 +115,7 @@
      * @param sessionDescription the session description of this call
      * @see ISipSessionListener
      */
-    void makeCall(in SipProfile callee,
-            in SessionDescription sessionDescription);
+    void makeCall(in SipProfile callee, String sessionDescription);
 
     /**
      * Answers an incoming call with the specified session description. The
@@ -125,7 +124,7 @@
      *
      * @param sessionDescription the session description to answer this call
      */
-    void answerCall(in SessionDescription sessionDescription);
+    void answerCall(String sessionDescription);
 
     /**
      * Ends an established call, terminates an outgoing call or rejects an
@@ -143,5 +142,5 @@
      *
      * @param sessionDescription the new session description
      */
-    void changeCall(in SessionDescription sessionDescription);
+    void changeCall(String sessionDescription);
 }
diff --git a/voip/java/android/net/sip/ISipSessionListener.aidl b/voip/java/android/net/sip/ISipSessionListener.aidl
index 8570958..c552a57 100644
--- a/voip/java/android/net/sip/ISipSessionListener.aidl
+++ b/voip/java/android/net/sip/ISipSessionListener.aidl
@@ -39,7 +39,7 @@
      * @param sessionDescription the caller's session description
      */
     void onRinging(in ISipSession session, in SipProfile caller,
-            in byte[] sessionDescription);
+            String sessionDescription);
 
     /**
      * Called when a RINGING response is received for the INVITE request sent
@@ -55,7 +55,7 @@
      * @param sessionDescription the peer's session description
      */
     void onCallEstablished(in ISipSession session,
-            in byte[] sessionDescription);
+            String sessionDescription);
 
     /**
      * Called when the session is terminated.
diff --git a/voip/java/android/net/sip/SdpSessionDescription.java b/voip/java/android/net/sip/SdpSessionDescription.java
index 0c29935..f6ae837 100644
--- a/voip/java/android/net/sip/SdpSessionDescription.java
+++ b/voip/java/android/net/sip/SdpSessionDescription.java
@@ -186,8 +186,8 @@
             }
         }
 
-        public SdpSessionDescription build() {
-            return mSdp;
+        public String build() {
+            return mSdp.toString();
         }
     }
 
diff --git a/voip/java/android/net/sip/SipAudioCall.java b/voip/java/android/net/sip/SipAudioCall.java
index 3cdd114..8254543 100644
--- a/voip/java/android/net/sip/SipAudioCall.java
+++ b/voip/java/android/net/sip/SipAudioCall.java
@@ -168,9 +168,9 @@
      * Attaches an incoming call to this call object.
      *
      * @param session the session that receives the incoming call
-     * @param sdp the session description of the incoming call
+     * @param sessionDescription the session description of the incoming call
      */
-    void attachCall(ISipSession session, SdpSessionDescription sdp)
+    void attachCall(ISipSession session, String sessionDescription)
             throws SipException;
 
     /** Ends a call. */
diff --git a/voip/java/android/net/sip/SipAudioCallImpl.java b/voip/java/android/net/sip/SipAudioCallImpl.java
index 5789cd4..a312f83 100644
--- a/voip/java/android/net/sip/SipAudioCallImpl.java
+++ b/voip/java/android/net/sip/SipAudioCallImpl.java
@@ -28,14 +28,6 @@
 import android.net.rtp.AudioGroup;
 import android.net.rtp.AudioStream;
 import android.net.rtp.RtpStream;
-import android.net.sip.ISipSession;
-import android.net.sip.SdpSessionDescription;
-import android.net.sip.SessionDescription;
-import android.net.sip.SipAudioCall;
-import android.net.sip.SipManager;
-import android.net.sip.SipProfile;
-import android.net.sip.SipSessionAdapter;
-import android.net.sip.SipSessionState;
 import android.net.wifi.WifiManager;
 import android.os.Message;
 import android.os.RemoteException;
@@ -200,7 +192,7 @@
 
     @Override
     public synchronized void onRinging(ISipSession session,
-            SipProfile peerProfile, byte[] sessionDescription) {
+            SipProfile peerProfile, String sessionDescription) {
         try {
             if ((mSipSession == null) || !mInCall
                     || !session.getCallId().equals(mSipSession.getCallId())) {
@@ -222,7 +214,7 @@
         }
     }
 
-    private synchronized void establishCall(byte[] sessionDescription) {
+    private synchronized void establishCall(String sessionDescription) {
         stopRingbackTone();
         stopRinging();
         try {
@@ -238,7 +230,7 @@
 
     @Override
     public void onCallEstablished(ISipSession session,
-            byte[] sessionDescription) {
+            String sessionDescription) {
         establishCall(sessionDescription);
         Listener listener = mListener;
         if (listener != null) {
@@ -316,10 +308,10 @@
     }
 
     public synchronized void attachCall(ISipSession session,
-            SdpSessionDescription sdp) throws SipException {
+            String sessionDescription) throws SipException {
         mSipSession = session;
-        mPeerSd = sdp;
         try {
+            mPeerSd = new SdpSessionDescription(sessionDescription);
             session.setListener(this);
         } catch (Throwable e) {
             Log.e(TAG, "attachCall()", e);
@@ -394,12 +386,12 @@
         if (audioGroup != null) audioGroup.setMode(AudioGroup.MODE_NORMAL);
     }
 
-    private SessionDescription createOfferSessionDescription() {
+    private String createOfferSessionDescription() {
         AudioCodec[] codecs = AudioCodec.getSystemSupportedCodecs();
         return createSdpBuilder(true, convert(codecs)).build();
     }
 
-    private SessionDescription createAnswerSessionDescription() {
+    private String createAnswerSessionDescription() {
         try {
             // choose an acceptable media from mPeerSd to answer
             SdpSessionDescription.AudioCodec codec = getCodec(mPeerSd);
@@ -416,7 +408,7 @@
         }
     }
 
-    private SessionDescription createHoldSessionDescription() {
+    private String createHoldSessionDescription() {
         try {
             return createSdpBuilder(false, mCodec)
                     .addMediaAttribute(AUDIO, "sendonly", (String) null)
@@ -448,7 +440,7 @@
         return (mWm.getConnectionInfo().getBSSID() == null) ? false : true;
     }
 
-    private SessionDescription createContinueSessionDescription() {
+    private String createContinueSessionDescription() {
         return createSdpBuilder(true, mCodec).build();
     }
 
diff --git a/voip/java/android/net/sip/SipManager.java b/voip/java/android/net/sip/SipManager.java
index 40792b9..700fb4e 100644
--- a/voip/java/android/net/sip/SipManager.java
+++ b/voip/java/android/net/sip/SipManager.java
@@ -298,21 +298,19 @@
             throw new SipException("Call ID missing in incoming call intent");
         }
 
-        byte[] offerSd = getOfferSessionDescription(incomingCallIntent);
+        String offerSd = getOfferSessionDescription(incomingCallIntent);
         if (offerSd == null) {
             throw new SipException("Session description missing in incoming "
                     + "call intent");
         }
 
         try {
-            SdpSessionDescription sdp = new SdpSessionDescription(offerSd);
-
             ISipSession session = mSipService.getPendingSession(callId);
             if (session == null) return null;
             SipAudioCall call = new SipAudioCallImpl(
                     context, session.getLocalProfile());
             call.setRingtoneEnabled(ringtoneEnabled);
-            call.attachCall(session, sdp);
+            call.attachCall(session, offerSd);
             call.setListener(listener);
             return call;
         } catch (Throwable t) {
@@ -329,7 +327,7 @@
     public static boolean isIncomingCallIntent(Intent intent) {
         if (intent == null) return false;
         String callId = getCallId(intent);
-        byte[] offerSd = getOfferSessionDescription(intent);
+        String offerSd = getOfferSessionDescription(intent);
         return ((callId != null) && (offerSd != null));
     }
 
@@ -351,8 +349,8 @@
      * @return the offer session description or null if the intent does not
      *      have it
      */
-    public static byte[] getOfferSessionDescription(Intent incomingCallIntent) {
-        return incomingCallIntent.getByteArrayExtra(OFFER_SD_KEY);
+    public static String getOfferSessionDescription(Intent incomingCallIntent) {
+        return incomingCallIntent.getStringExtra(OFFER_SD_KEY);
     }
 
     /**
@@ -365,7 +363,7 @@
      * @hide
      */
     public static Intent createIncomingCallBroadcast(String action,
-            String callId, byte[] sessionDescription) {
+            String callId, String sessionDescription) {
         Intent intent = new Intent(action);
         intent.putExtra(CALL_ID_KEY, callId);
         intent.putExtra(OFFER_SD_KEY, sessionDescription);
diff --git a/voip/java/android/net/sip/SipProfile.java b/voip/java/android/net/sip/SipProfile.java
index 6c99141..aa2da75 100644
--- a/voip/java/android/net/sip/SipProfile.java
+++ b/voip/java/android/net/sip/SipProfile.java
@@ -46,8 +46,7 @@
     private String mProfileName;
     private boolean mSendKeepAlive = false;
     private boolean mAutoRegistration = true;
-    private boolean mAllowOutgoingCall = false;
-    private int mCallingUid = -1;
+    private transient int mCallingUid = 0;
 
     /** @hide */
     public static final Parcelable.Creator<SipProfile> CREATOR =
@@ -245,18 +244,6 @@
         }
 
         /**
-         * Sets the allow-outgoing-call flag.
-         *
-         * @param flag true if allowing to make outgoing call on the profile;
-         *      false otherwise
-         * @return this builder object
-         */
-        public Builder setOutgoingCallAllowed(boolean flag) {
-            mProfile.mAllowOutgoingCall = flag;
-            return this;
-        }
-
-        /**
          * Builds and returns the SIP profile object.
          *
          * @return the profile object created
@@ -293,7 +280,6 @@
         mProfileName = in.readString();
         mSendKeepAlive = (in.readInt() == 0) ? false : true;
         mAutoRegistration = (in.readInt() == 0) ? false : true;
-        mAllowOutgoingCall = (in.readInt() == 0) ? false : true;
         mCallingUid = in.readInt();
     }
 
@@ -307,7 +293,6 @@
         out.writeString(mProfileName);
         out.writeInt(mSendKeepAlive ? 1 : 0);
         out.writeInt(mAutoRegistration ? 1 : 0);
-        out.writeInt(mAllowOutgoingCall ? 1 : 0);
         out.writeInt(mCallingUid);
     }
 
@@ -435,13 +420,6 @@
     }
 
     /**
-     * Returns true if allowing to make outgoing calls on this profile.
-     */
-    public boolean isOutgoingCallAllowed() {
-        return mAllowOutgoingCall;
-    }
-
-    /**
      * Sets the calling process's Uid in the sip service.
      * @hide
      */
diff --git a/voip/java/android/net/sip/SipSessionAdapter.java b/voip/java/android/net/sip/SipSessionAdapter.java
index cfb71d7..770d4eb 100644
--- a/voip/java/android/net/sip/SipSessionAdapter.java
+++ b/voip/java/android/net/sip/SipSessionAdapter.java
@@ -26,14 +26,14 @@
     }
 
     public void onRinging(ISipSession session, SipProfile caller,
-            byte[] sessionDescription) {
+            String sessionDescription) {
     }
 
     public void onRingingBack(ISipSession session) {
     }
 
-    public void onCallEstablished(
-            ISipSession session, byte[] sessionDescription) {
+    public void onCallEstablished(ISipSession session,
+            String sessionDescription) {
     }
 
     public void onCallEnded(ISipSession session) {