Make vCard 4.0 parser support SORT-AS parameter.

Add unit test for it.

Fix bugs in foundation classes..

Change-Id: I8b5ca1fd49ef3e729ec85429fb8110efde5091f1
diff --git a/java/com/android/vcard/VCardConfig.java b/java/com/android/vcard/VCardConfig.java
index 7ebb365..e833b67 100644
--- a/java/com/android/vcard/VCardConfig.java
+++ b/java/com/android/vcard/VCardConfig.java
@@ -304,6 +304,7 @@
 
     /**
      * 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);
diff --git a/java/com/android/vcard/VCardConstants.java b/java/com/android/vcard/VCardConstants.java
index 6a7b947..20cc093 100644
--- a/java/com/android/vcard/VCardConstants.java
+++ b/java/com/android/vcard/VCardConstants.java
@@ -141,6 +141,11 @@
     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";
diff --git a/java/com/android/vcard/VCardEntry.java b/java/com/android/vcard/VCardEntry.java
index a9ecd70..b7bd1ef 100644
--- a/java/com/android/vcard/VCardEntry.java
+++ b/java/com/android/vcard/VCardEntry.java
@@ -249,17 +249,20 @@
         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;
         }
 
@@ -428,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;
@@ -523,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
@@ -546,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;
         }
@@ -581,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) {
@@ -599,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);
     }
 
     /**
@@ -613,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) {
@@ -624,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,
@@ -650,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) {
@@ -662,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,7 +832,7 @@
             // actually exist in the real vCard data, does not exist.
             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) ||
@@ -769,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.isVersion30(mVCardType));
+                        VCardUtils.constructListFromValue(propValue, mVCardType);
                 handlePhoneticNameFromSound(phoneticNameList);
             } else {
                 // Ignore this field since Android cannot understand what it is.
@@ -871,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)) {
@@ -970,8 +1047,7 @@
             mPhoneticFamilyName = propValue;
         } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
             final List<String> customPropertyList =
-                VCardUtils.constructListFromValue(propValue,
-                        VCardConfig.isVersion30(mVCardType));
+                VCardUtils.constructListFromValue(propValue, mVCardType);
             handleAndroidCustomProperty(customPropertyList);
         } else {
         }
@@ -1110,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);
                 }
diff --git a/java/com/android/vcard/VCardParserImpl_V21.java b/java/com/android/vcard/VCardParserImpl_V21.java
index ff96061..f030c6e 100644
--- a/java/com/android/vcard/VCardParserImpl_V21.java
+++ b/java/com/android/vcard/VCardParserImpl_V21.java
@@ -32,6 +32,7 @@
 import java.io.Reader;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -43,6 +44,42 @@
 /* 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;
 
@@ -267,21 +304,19 @@
         if (!readBeginVCard(allowGarbage)) {
             return false;
         }
-        long start;
-        if (mInterpreter != null) {
-            start = System.currentTimeMillis();
-            mInterpreter.startEntry();
-            mTimeReadStartRecord += System.currentTimeMillis() - start;
-        }
-        start = System.currentTimeMillis();
+        final long beforeStartEntry = System.currentTimeMillis();
+        mInterpreter.startEntry();
+        mTimeReadStartRecord += System.currentTimeMillis() - beforeStartEntry;
+
+        final long beforeParseItems = System.currentTimeMillis();
         parseItems();
-        mTimeParseItems += System.currentTimeMillis() - start;
+        mTimeParseItems += System.currentTimeMillis() - beforeParseItems;
+
         readEndVCard(true, false);
-        if (mInterpreter != null) {
-            start = System.currentTimeMillis();
-            mInterpreter.endEntry();
-            mTimeReadEndRecord += System.currentTimeMillis() - start;
-        }
+
+        final long beforeEndEntry = System.currentTimeMillis();
+        mInterpreter.endEntry();
+        mTimeReadEndRecord += System.currentTimeMillis() - beforeEndEntry;
         return true;
     }
 
@@ -375,34 +410,31 @@
     protected void parseItems() throws IOException, VCardException {
         boolean ended = false;
 
-        if (mInterpreter != null) {
-            long start = System.currentTimeMillis();
-            mInterpreter.startProperty();
-            mTimeStartProperty += System.currentTimeMillis() - start;
-        }
+        final long beforeBeginProperty = System.currentTimeMillis();
+        mInterpreter.startProperty();
+        mTimeStartProperty += System.currentTimeMillis() - beforeBeginProperty;
         ended = parseItem();
-        if (mInterpreter != null && !ended) {
-            long start = System.currentTimeMillis();
+        if (!ended) {
+            final long beforeEndProperty = System.currentTimeMillis();
             mInterpreter.endProperty();
-            mTimeEndProperty += System.currentTimeMillis() - start;
+            mTimeEndProperty += System.currentTimeMillis() - beforeEndProperty;
         }
 
         while (!ended) {
-            if (mInterpreter != null) {
-                long start = System.currentTimeMillis();
-                mInterpreter.startProperty();
-                mTimeStartProperty += System.currentTimeMillis() - start;
-            }
+            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 (mInterpreter != null && !ended) {
-                long start = System.currentTimeMillis();
+
+            if (!ended) {
+                final long beforeEndProperty = System.currentTimeMillis();
                 mInterpreter.endProperty();
-                mTimeEndProperty += System.currentTimeMillis() - start;
+                mTimeEndProperty += System.currentTimeMillis() - beforeEndProperty;
             }
         }
     }
@@ -487,9 +519,7 @@
                             mPreviousLine = line;
                             return null;
                         }
-                        if (mInterpreter != null) {
-                            mInterpreter.propertyName(propertyName);
-                        }
+                        mInterpreter.propertyName(propertyName);
                         propertyNameAndValue[0] = propertyName;
                         if (i < length - 1) {
                             propertyNameAndValue[1] = line.substring(i + 1);
@@ -501,7 +531,7 @@
                         final String groupName = line.substring(nameIndex, i);
                         if (groupName.length() == 0) {
                             Log.w(LOG_TAG, "Empty group found. Ignoring.");
-                        } else if (mInterpreter != null) {
+                        } else {
                             mInterpreter.propertyGroup(groupName);
                         }
                         nameIndex = i + 1;  // Next should be another group or a property name.
@@ -511,9 +541,7 @@
                             mPreviousLine = line;
                             return null;
                         }
-                        if (mInterpreter != null) {
-                            mInterpreter.propertyName(propertyName);
-                        }
+                        mInterpreter.propertyName(propertyName);
                         propertyNameAndValue[0] = propertyName;
                         nameIndex = i + 1;
                         state = STATE_PARAMS;  // Start parameter parsing.
@@ -608,10 +636,8 @@
             mUnknownTypeSet.add(ptypeval);
             Log.w(LOG_TAG, String.format("TYPE unsupported by %s: ", getVersion(), ptypeval));
         }
-        if (mInterpreter != null) {
-            mInterpreter.propertyParamType("TYPE");
-            mInterpreter.propertyParamValue(ptypeval);
-        }
+        mInterpreter.propertyParamType("TYPE");
+        mInterpreter.propertyParamValue(ptypeval);
     }
 
     /*
@@ -625,10 +651,8 @@
             Log.w(LOG_TAG, String.format(
                     "The value unsupported by TYPE of %s: ", getVersion(), pvalueval));
         }
-        if (mInterpreter != null) {
-            mInterpreter.propertyParamType("VALUE");
-            mInterpreter.propertyParamValue(pvalueval);
-        }
+        mInterpreter.propertyParamType("VALUE");
+        mInterpreter.propertyParamValue(pvalueval);
     }
 
     /*
@@ -637,10 +661,8 @@
     protected void handleEncoding(String pencodingval) throws VCardException {
         if (getAvailableEncodingSet().contains(pencodingval) ||
                 pencodingval.startsWith("X-")) {
-            if (mInterpreter != null) {
-                mInterpreter.propertyParamType("ENCODING");
-                mInterpreter.propertyParamValue(pencodingval);
-            }
+            mInterpreter.propertyParamType("ENCODING");
+            mInterpreter.propertyParamValue(pencodingval);
             mCurrentEncoding = pencodingval;
         } else {
             throw new VCardException("Unknown encoding \"" + pencodingval + "\"");
@@ -655,10 +677,8 @@
      * </p>
      */
     protected void handleCharset(String charsetval) {
-        if (mInterpreter != null) {
-            mInterpreter.propertyParamType("CHARSET");
-            mInterpreter.propertyParamValue(charsetval);
-        }
+        mInterpreter.propertyParamType("CHARSET");
+        mInterpreter.propertyParamValue(charsetval);
     }
 
     /**
@@ -683,10 +703,8 @@
                 throw new VCardException("Invalid Language: \"" + langval + "\"");
             }
         }
-        if (mInterpreter != null) {
-            mInterpreter.propertyParamType("LANGUAGE");
-            mInterpreter.propertyParamValue(langval);
-        }
+        mInterpreter.propertyParamType(VCardConstants.PARAM_LANGUAGE);
+        mInterpreter.propertyParamValue(langval);
     }
 
     private boolean isAsciiLetter(char ch) {
@@ -700,10 +718,8 @@
      * Mainly for "X-" type. This accepts any kind of type without check.
      */
     protected void handleAnyParam(String paramName, String paramValue) {
-        if (mInterpreter != null) {
-            mInterpreter.propertyParamType(paramName);
-            mInterpreter.propertyParamValue(paramValue);
-        }
+        mInterpreter.propertyParamType(paramName);
+        mInterpreter.propertyParamValue(paramValue);
     }
 
     protected void handlePropertyValue(String propertyName, String propertyValue)
@@ -712,11 +728,9 @@
         if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_QP)) {
             final long start = System.currentTimeMillis();
             final String result = getQuotedPrintable(propertyValue);
-            if (mInterpreter != null) {
-                ArrayList<String> v = new ArrayList<String>();
-                v.add(result);
-                mInterpreter.propertyValues(v);
-            }
+            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)) {
@@ -724,17 +738,12 @@
             // 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 (mInterpreter != null) {
-                    ArrayList<String> arrayList = new ArrayList<String>();
-                    arrayList.add(result);
-                    mInterpreter.propertyValues(arrayList);
-                }
+                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!");
-                if (mInterpreter != null) {
-                    mInterpreter.propertyValues(null);
-                }
+                mInterpreter.propertyValues(null);
             }
             mTimeHandleBase64 += System.currentTimeMillis() - start;
         } else {
@@ -797,11 +806,9 @@
             }
 
             final long start = System.currentTimeMillis();
-            if (mInterpreter != null) {
-                ArrayList<String> v = new ArrayList<String>();
-                v.add(maybeUnescapeText(propertyValue));
-                mInterpreter.propertyValues(v);
-            }
+            ArrayList<String> v = new ArrayList<String>();
+            v.add(maybeUnescapeText(propertyValue));
+            mInterpreter.propertyValues(v);
             mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start;
         }
     }
@@ -908,10 +915,8 @@
             propertyValue = getQuotedPrintable(propertyValue);
         }
 
-        if (mInterpreter != null) {
-            mInterpreter.propertyValues(VCardUtils.constructListFromValue(propertyValue,
-                    (getVersion() == VCardConfig.VERSION_30)));
-        }
+        mInterpreter.propertyValues(VCardUtils.constructListFromValue(propertyValue,
+                getVersion()));
     }
 
     /*
@@ -943,7 +948,7 @@
      * null otherwise. e.g. In vCard 2.1, "\;" should be unescaped into ";"
      * while "\x" should not be.
      */
-    protected String maybeEscapeCharacter(final char ch) {
+    protected String maybeUnescapeCharacter(final char ch) {
         return unescapeCharacter(ch);
     }
 
@@ -1019,7 +1024,7 @@
         final InputStreamReader tmpReader = new InputStreamReader(is, mIntermediateCharset);
         mReader = new CustomBufferedReader(tmpReader);
 
-        mInterpreter = interpreter;
+        mInterpreter = (interpreter != null ? interpreter : new EmptyInterpreter());
 
         final long start = System.currentTimeMillis();
         if (mInterpreter != null) {
diff --git a/java/com/android/vcard/VCardParserImpl_V30.java b/java/com/android/vcard/VCardParserImpl_V30.java
index 9fefe89..648ce7e 100644
--- a/java/com/android/vcard/VCardParserImpl_V30.java
+++ b/java/com/android/vcard/VCardParserImpl_V30.java
@@ -179,12 +179,13 @@
 
     @Override
     protected void handleAnyParam(final String paramName, final String paramValue) {
-        super.handleAnyParam(paramName, paramValue);
+        mInterpreter.propertyParamType(paramName);
+        splitAndPutParamValue(paramValue);
     }
 
     @Override
-    protected void handleParamWithoutName(final String paramValue) throws VCardException {
-        super.handleParamWithoutName(paramValue);
+    protected void handleParamWithoutName(final String paramValue) {
+        handleType(paramValue);
     }
 
     /*
@@ -200,73 +201,86 @@
      *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\").
      */
     @Override
-    protected void handleType(final String paramvalues) {
-        if (mInterpreter != null) {
-            mInterpreter.propertyParamType("TYPE");
+    protected void handleType(final String paramValue) {
+        mInterpreter.propertyParamType("TYPE");
+        splitAndPutParamValue(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 = paramvalues.length();
-            for (int i = 0; i < length; i = paramvalues.offsetByCodePoints(i, 1)) {
-                final int codePoint = paramvalues.codePointAt(i);
-                if (codePoint == '"') {
-                    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 (codePoint == ',' && !insideDquote) {
-                    if (builder == null) {
-                        Log.w(LOG_TAG, "Comma is used before actual string comes. (" +
-                                paramvalues + ")");
-                    } else {
-                        mInterpreter.propertyParamValue(builder.toString());
-                        builder = null;
-                    }
+    /**
+     * 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 {
-                    // To stop creating empty StringBuffer at the end of parameter,
-                    // we delay creating this object until this point.
-                    if (builder == null) {
-                        builder = new StringBuilder();
+                    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());
+                        }
                     }
-                    builder.appendCodePoint(codePoint);
+                    insideDquote = true;
                 }
-            }
-            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 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());
             }
         }
     }
@@ -356,11 +370,11 @@
     }
 
     @Override
-    protected String maybeEscapeCharacter(final char ch) {
-        return escapeCharacter(ch);
+    protected String maybeUnescapeCharacter(final char ch) {
+        return unescapeCharacter(ch);
     }
 
-    public static String escapeCharacter(final char ch) {
+    public static String unescapeCharacter(final char ch) {
         if (ch == 'n' || ch == 'N') {
             return "\n";
         } else {
diff --git a/java/com/android/vcard/VCardParserImpl_V40.java b/java/com/android/vcard/VCardParserImpl_V40.java
index 73bb9c8..3a49444 100644
--- a/java/com/android/vcard/VCardParserImpl_V40.java
+++ b/java/com/android/vcard/VCardParserImpl_V40.java
@@ -77,6 +77,14 @@
         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;
diff --git a/java/com/android/vcard/VCardParser_V40.java b/java/com/android/vcard/VCardParser_V40.java
index 6431d91..700cdfd 100644
--- a/java/com/android/vcard/VCardParser_V40.java
+++ b/java/com/android/vcard/VCardParser_V40.java
@@ -41,7 +41,7 @@
                     "LABEL", "TEL", "EMAIL", "IMPP", "LANG", "TZ",
                     "GEO", "TITLE", "ROLE", "LOGO", "ORG", "MEMBER",
                     "RELATED", "CATEGORIES", "NOTE", "PRODID",
-                    "REV", "SORT-STRING", "SOUND", "UID", "CLIENTPIDMAP",
+                    "REV", "SOUND", "UID", "CLIENTPIDMAP",
                     "URL", "VERSION", "CLASS", "KEY", "FBURL", "CALENDRURI",
                     "CALURI")));
 
diff --git a/java/com/android/vcard/VCardUtils.java b/java/com/android/vcard/VCardUtils.java
index b869d76..f3f5bdf 100644
--- a/java/com/android/vcard/VCardUtils.java
+++ b/java/com/android/vcard/VCardUtils.java
@@ -332,18 +332,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 ? VCardParserImpl_V30.escapeCharacter(nextCh) :
-                        VCardParserImpl_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++;
diff --git a/tests/res/raw/v40_sort_as.vcf b/tests/res/raw/v40_sort_as.vcf
new file mode 100644
index 0000000..6f6bc3b
--- /dev/null
+++ b/tests/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/tests/src/com/android/vcard/tests/VCardImporterTests.java b/tests/src/com/android/vcard/tests/VCardImporterTests.java
index fcff4fc..1148e57 100644
--- a/tests/src/com/android/vcard/tests/VCardImporterTests.java
+++ b/tests/src/com/android/vcard/tests/VCardImporterTests.java
@@ -1037,12 +1037,41 @@
                 .put(Phone.NUMBER, "1");
     }
 
-    public void testCommaSeparatedParamsV30_Parse() {
-        mVerifier.initForImportTest(V30, R.raw.v30_comma_separated);
+    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("N", Arrays.asList("F", "G", "M", "", ""),
-                        new TypeSet("PREF", "HOME"))
-                .addExpectedNodeWithOrder("TEL", "1",
-                        new TypeSet("COMMA,SEPARATED:INSIDE.DQUOTE", "PREF"));
+                .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");
     }
 }
diff --git a/tests/src/com/android/vcard/tests/VCardTestsBase.java b/tests/src/com/android/vcard/tests/VCardTestsBase.java
index 5e5ecec..dbb409f 100644
--- a/tests/src/com/android/vcard/tests/VCardTestsBase.java
+++ b/tests/src/com/android/vcard/tests/VCardTestsBase.java
@@ -28,6 +28,7 @@
 public class VCardTestsBase extends AndroidTestCase {
     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;
diff --git a/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java b/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java
index fb53b8f..74cc3e5 100644
--- a/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java
+++ b/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java
@@ -33,6 +33,7 @@
         return this;
     }
 
+    /*
     public ContentValuesBuilder put(String key, Byte value) {
         mContentValues.put(key, value);
         return this;
@@ -41,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;
@@ -66,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/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.java b/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.java
index 57dbf32..f69ea1b 100644
--- a/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.java
+++ b/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.java
@@ -29,9 +29,9 @@
     private int mIndex;
 
     public ContentValuesVerifierElem addElem(AndroidTestCase androidTestCase) {
-        ContentValuesVerifierElem importVerifier = new ContentValuesVerifierElem(androidTestCase);
-        mContentValuesVerifierElemList.add(importVerifier);
-        return importVerifier;
+        ContentValuesVerifierElem elem = new ContentValuesVerifierElem(androidTestCase);
+        mContentValuesVerifierElemList.add(elem);
+        return elem;
     }
 
     public void onStart() {
diff --git a/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java b/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java
index cb3dfef..8d6fa12 100644
--- a/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java
+++ b/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java
@@ -37,7 +37,6 @@
 import android.test.AndroidTestCase;
 import android.test.mock.MockContentProvider;
 import android.text.TextUtils;
-import android.util.Log;
 
 import junit.framework.TestCase;
 
@@ -171,9 +170,11 @@
                 }
                 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));
                     }
@@ -240,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()) {
@@ -253,17 +254,18 @@
             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;
-                    Log.d("@@@", "expected (len: " + e.length + "): " + Arrays.toString(e));
-                    Log.d("@@@", "actual (len: " + a.length + "): " + Arrays.toString(a));
                     return false;
                 }
-            } else if (!value.equals(actual.get(key))) {
-                Log.d("@@@", "different.");
+            } else if (!value.equals(actualValue) &&
+                    !value.toString().equals(actualValue.toString())) {
                 return false;
             }
         }
diff --git a/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.java b/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.java
index b6a4138..d0fb83c 100644
--- a/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.java
+++ b/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.java
@@ -100,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,
diff --git a/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java b/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java
index e5de13f..1b5cb7e 100644
--- a/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java
+++ b/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java
@@ -235,15 +235,13 @@
     private void verifyWithInputStream(InputStream is) throws IOException {
         final VCardInterpreter interpreter;
         if (mContentValuesVerifier != null) {
-            final VNodeBuilder vnodeBuilder = mPropertyNodesVerifier;
-            final VCardEntryConstructor vcardDataBuilder =
-                    new VCardEntryConstructor(mVCardType);
-            vcardDataBuilder.addEntryHandler(mContentValuesVerifier);
+            final VCardEntryConstructor constructor = new VCardEntryConstructor(mVCardType);
+            constructor.addEntryHandler(mContentValuesVerifier);
             if (mPropertyNodesVerifier != null) {
                 interpreter = new VCardInterpreterCollection(Arrays.asList(
-                        mPropertyNodesVerifier, vcardDataBuilder));
+                        mPropertyNodesVerifier, constructor));
             } else {
-                interpreter = vnodeBuilder;
+                interpreter = constructor;
             }
         } else {
             if (mPropertyNodesVerifier != null) {