Copy VCard utilities
Ben Komalo [Thu, 28 Oct 2010 18:28:05 +0000 (11:28 -0700)]
from goog/master:frameworks/opt/vcard/java/com/android/vcard
and CharsetUtils

Some massaging done.

Change-Id: I961294ace15f613402a6e7b26c6c4908465ab520

28 files changed:
src/com/android/vcard/CharsetUtils.java [new file with mode: 0644]
src/com/android/vcard/JapaneseUtils.java [new file with mode: 0644]
src/com/android/vcard/VCardBuilder.java [new file with mode: 0644]
src/com/android/vcard/VCardConfig.java [new file with mode: 0644]
src/com/android/vcard/VCardConstants.java [new file with mode: 0644]
src/com/android/vcard/VCardEntry.java [new file with mode: 0644]
src/com/android/vcard/VCardEntryCommitter.java [new file with mode: 0644]
src/com/android/vcard/VCardEntryConstructor.java [new file with mode: 0644]
src/com/android/vcard/VCardEntryCounter.java [new file with mode: 0644]
src/com/android/vcard/VCardEntryHandler.java [new file with mode: 0644]
src/com/android/vcard/VCardInterpreter.java [new file with mode: 0644]
src/com/android/vcard/VCardInterpreterCollection.java [new file with mode: 0644]
src/com/android/vcard/VCardParser.java [new file with mode: 0644]
src/com/android/vcard/VCardParserImpl_V21.java [new file with mode: 0644]
src/com/android/vcard/VCardParserImpl_V30.java [new file with mode: 0644]
src/com/android/vcard/VCardParserImpl_V40.java [new file with mode: 0644]
src/com/android/vcard/VCardParser_V21.java [new file with mode: 0644]
src/com/android/vcard/VCardParser_V30.java [new file with mode: 0644]
src/com/android/vcard/VCardParser_V40.java [new file with mode: 0644]
src/com/android/vcard/VCardSourceDetector.java [new file with mode: 0644]
src/com/android/vcard/VCardUtils.java [new file with mode: 0644]
src/com/android/vcard/exception/VCardAgentNotSupportedException.java [new file with mode: 0644]
src/com/android/vcard/exception/VCardException.java [new file with mode: 0644]
src/com/android/vcard/exception/VCardInvalidCommentLineException.java [new file with mode: 0644]
src/com/android/vcard/exception/VCardInvalidLineException.java [new file with mode: 0644]
src/com/android/vcard/exception/VCardNestedException.java [new file with mode: 0644]
src/com/android/vcard/exception/VCardNotSupportedException.java [new file with mode: 0644]
src/com/android/vcard/exception/VCardVersionException.java [new file with mode: 0644]

diff --git a/src/com/android/vcard/CharsetUtils.java b/src/com/android/vcard/CharsetUtils.java
new file mode 100644 (file)
index 0000000..4c85b29
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * 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 com.android.vcard;
+
+import android.os.Build;
+import android.text.TextUtils;
+
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <p>
+ * A class containing utility methods related to character sets. This
+ * class is primarily useful for code that wishes to be vendor-aware
+ * in its interpretation of Japanese charset names (used in DoCoMo,
+ * KDDI, and SoftBank).
+ * </p>
+ *
+ * <p>
+ * <b>Note:</b> Developers will need to add an appropriate mapping for
+ * each vendor-specific charset. You may need to modify the C libraries
+ * like icu4c in order to let Android support an additional charset.
+ * </p>
+ *
+ * @hide
+ */
+public final class CharsetUtils {
+    /**
+     * name of the vendor "DoCoMo". <b>Note:</b> This isn't a public
+     * constant, in order to keep this class from becoming a de facto
+     * reference list of vendor names.
+     */
+    private static final String VENDOR_DOCOMO = "docomo";
+    /**
+     * Name of the vendor "KDDI".
+     */
+    private static final String VENDOR_KDDI = "kddi";
+    /**
+     * Name of the vendor "SoftBank".
+     */
+    private static final String VENDOR_SOFTBANK = "softbank";
+
+    /**
+     * Represents one-to-one mapping from a vendor name to a charset specific to the vendor.
+     */
+    private static final Map<String, String> sVendorShiftJisMap = new HashMap<String, String>();
+
+    static {
+        // These variants of Shift_JIS come from icu's mapping data (convrtrs.txt)
+        sVendorShiftJisMap.put(VENDOR_DOCOMO, "docomo-shift_jis-2007");
+        sVendorShiftJisMap.put(VENDOR_KDDI, "kddi-shift_jis-2007");
+        sVendorShiftJisMap.put(VENDOR_SOFTBANK, "softbank-shift_jis-2007");
+    }
+
+    /**
+     * This class is uninstantiable.
+     */
+    private CharsetUtils() {
+        // This space intentionally left blank.
+    }
+
+    /**
+     * Returns the name of the vendor-specific character set
+     * corresponding to the given original character set name and
+     * vendor. If there is no vendor-specific character set for the
+     * given name/vendor pair, this returns the original character set name.
+     *
+     * @param charsetName the base character set name
+     * @param vendor the vendor to specialize for. All characters should be lower-cased.
+     * @return the specialized character set name, or {@code charsetName} if
+     * there is no specialized name
+     */
+    public static String nameForVendor(String charsetName, String vendor) {
+        if (!TextUtils.isEmpty(charsetName) && !TextUtils.isEmpty(vendor)) {
+            // You can add your own mapping here.
+            if (isShiftJis(charsetName)) {
+                final String vendorShiftJis = sVendorShiftJisMap.get(vendor);
+                if (vendorShiftJis != null) {
+                    return vendorShiftJis;
+                }
+            }
+        }
+
+        return charsetName;
+    }
+
+    /**
+     * Returns the name of the vendor-specific character set
+     * corresponding to the given original character set name and the
+     * default vendor (that is, the targeted vendor of the device this
+     * code is running on). This method merely calls through to
+     * {@link #nameForVendor(String,String)}, passing the default vendor
+     * as the second argument.
+     *
+     * @param charsetName the base character set name
+     * @return the specialized character set name, or {@code charsetName} if
+     * there is no specialized name
+     */
+    public static String nameForDefaultVendor(String charsetName) {
+        return nameForVendor(charsetName, getDefaultVendor());
+    }
+
+    /**
+     * Returns the vendor-specific character set corresponding to the
+     * given original character set name and vendor. If there is no
+     * vendor-specific character set for the given name/vendor pair,
+     * this returns the character set corresponding to the original
+     * name. The vendor name is matched case-insensitively. This
+     * method merely calls {@code Charset.forName()} on a name
+     * transformed by a call to {@link #nameForVendor(String,String)}.
+     *
+     * @param charsetName the base character set name
+     * @param vendor the vendor to specialize for
+     * @return the specialized character set, or the one corresponding
+     * directly to {@code charsetName} if there is no specialized
+     * variant
+     * @throws UnsupportedCharsetException thrown if the named character
+     * set is not supported by the system
+     * @throws IllegalCharsetNameException thrown if {@code charsetName}
+     * has invalid syntax
+     */
+    public static Charset charsetForVendor(String charsetName, String vendor)
+            throws UnsupportedCharsetException, IllegalCharsetNameException {
+        charsetName = nameForVendor(charsetName, vendor);
+        return Charset.forName(charsetName);
+    }
+
+    /**
+     * Returns the vendor-specific character set corresponding to the
+     * given original character set name and default vendor (that is,
+     * the targeted vendor of the device this code is running on).
+     * This method merely calls through to {@link
+     * #charsetForVendor(String,String)}, passing the default vendor
+     * as the second argument.
+     *
+     * @param charsetName the base character set name
+     * @return the specialized character set, or the one corresponding
+     * directly to {@code charsetName} if there is no specialized
+     * variant
+     * @throws UnsupportedCharsetException thrown if the named character
+     * set is not supported by the system
+     * @throws IllegalCharsetNameException thrown if {@code charsetName}
+     * has invalid syntax
+     */
+    public static Charset charsetForVendor(String charsetName)
+            throws UnsupportedCharsetException, IllegalCharsetNameException {
+        return charsetForVendor(charsetName, getDefaultVendor());
+    }
+
+    /**
+     * Returns whether the given character set name indicates the Shift-JIS
+     * encoding. Returns false if the name is null.
+     *
+     * @param charsetName the character set name
+     * @return {@code true} if the name corresponds to Shift-JIS or
+     * {@code false} if not
+     */
+    private static boolean isShiftJis(String charsetName) {
+        // Bail quickly if the length doesn't match.
+        if (charsetName == null) {
+            return false;
+        }
+        int length = charsetName.length();
+        if (length != 4 && length != 9) {
+            return false;
+        }
+
+        return charsetName.equalsIgnoreCase("shift_jis")
+            || charsetName.equalsIgnoreCase("shift-jis")
+            || charsetName.equalsIgnoreCase("sjis");
+    }
+
+    /**
+     * Gets the default vendor for this build.
+     *
+     * @return the default vendor name
+     */
+    private static String getDefaultVendor() {
+        return Build.BRAND;
+    }
+}
diff --git a/src/com/android/vcard/JapaneseUtils.java b/src/com/android/vcard/JapaneseUtils.java
new file mode 100644 (file)
index 0000000..5b44944
--- /dev/null
@@ -0,0 +1,379 @@
+/*
+ * 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 com.android.vcard;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * TextUtils especially for Japanese.
+ */
+/* package */ class JapaneseUtils {
+    static private final Map<Character, String> sHalfWidthMap =
+        new HashMap<Character, String>();
+
+    static {
+        sHalfWidthMap.put('\u3001', "\uFF64");
+        sHalfWidthMap.put('\u3002', "\uFF61");
+        sHalfWidthMap.put('\u300C', "\uFF62");
+        sHalfWidthMap.put('\u300D', "\uFF63");
+        sHalfWidthMap.put('\u301C', "~");
+        sHalfWidthMap.put('\u3041', "\uFF67");
+        sHalfWidthMap.put('\u3042', "\uFF71");
+        sHalfWidthMap.put('\u3043', "\uFF68");
+        sHalfWidthMap.put('\u3044', "\uFF72");
+        sHalfWidthMap.put('\u3045', "\uFF69");
+        sHalfWidthMap.put('\u3046', "\uFF73");
+        sHalfWidthMap.put('\u3047', "\uFF6A");
+        sHalfWidthMap.put('\u3048', "\uFF74");
+        sHalfWidthMap.put('\u3049', "\uFF6B");
+        sHalfWidthMap.put('\u304A', "\uFF75");
+        sHalfWidthMap.put('\u304B', "\uFF76");
+        sHalfWidthMap.put('\u304C', "\uFF76\uFF9E");
+        sHalfWidthMap.put('\u304D', "\uFF77");
+        sHalfWidthMap.put('\u304E', "\uFF77\uFF9E");
+        sHalfWidthMap.put('\u304F', "\uFF78");
+        sHalfWidthMap.put('\u3050', "\uFF78\uFF9E");
+        sHalfWidthMap.put('\u3051', "\uFF79");
+        sHalfWidthMap.put('\u3052', "\uFF79\uFF9E");
+        sHalfWidthMap.put('\u3053', "\uFF7A");
+        sHalfWidthMap.put('\u3054', "\uFF7A\uFF9E");
+        sHalfWidthMap.put('\u3055', "\uFF7B");
+        sHalfWidthMap.put('\u3056', "\uFF7B\uFF9E");
+        sHalfWidthMap.put('\u3057', "\uFF7C");
+        sHalfWidthMap.put('\u3058', "\uFF7C\uFF9E");
+        sHalfWidthMap.put('\u3059', "\uFF7D");
+        sHalfWidthMap.put('\u305A', "\uFF7D\uFF9E");
+        sHalfWidthMap.put('\u305B', "\uFF7E");
+        sHalfWidthMap.put('\u305C', "\uFF7E\uFF9E");
+        sHalfWidthMap.put('\u305D', "\uFF7F");
+        sHalfWidthMap.put('\u305E', "\uFF7F\uFF9E");
+        sHalfWidthMap.put('\u305F', "\uFF80");
+        sHalfWidthMap.put('\u3060', "\uFF80\uFF9E");
+        sHalfWidthMap.put('\u3061', "\uFF81");
+        sHalfWidthMap.put('\u3062', "\uFF81\uFF9E");
+        sHalfWidthMap.put('\u3063', "\uFF6F");
+        sHalfWidthMap.put('\u3064', "\uFF82");
+        sHalfWidthMap.put('\u3065', "\uFF82\uFF9E");
+        sHalfWidthMap.put('\u3066', "\uFF83");
+        sHalfWidthMap.put('\u3067', "\uFF83\uFF9E");
+        sHalfWidthMap.put('\u3068', "\uFF84");
+        sHalfWidthMap.put('\u3069', "\uFF84\uFF9E");
+        sHalfWidthMap.put('\u306A', "\uFF85");
+        sHalfWidthMap.put('\u306B', "\uFF86");
+        sHalfWidthMap.put('\u306C', "\uFF87");
+        sHalfWidthMap.put('\u306D', "\uFF88");
+        sHalfWidthMap.put('\u306E', "\uFF89");
+        sHalfWidthMap.put('\u306F', "\uFF8A");
+        sHalfWidthMap.put('\u3070', "\uFF8A\uFF9E");
+        sHalfWidthMap.put('\u3071', "\uFF8A\uFF9F");
+        sHalfWidthMap.put('\u3072', "\uFF8B");
+        sHalfWidthMap.put('\u3073', "\uFF8B\uFF9E");
+        sHalfWidthMap.put('\u3074', "\uFF8B\uFF9F");
+        sHalfWidthMap.put('\u3075', "\uFF8C");
+        sHalfWidthMap.put('\u3076', "\uFF8C\uFF9E");
+        sHalfWidthMap.put('\u3077', "\uFF8C\uFF9F");
+        sHalfWidthMap.put('\u3078', "\uFF8D");
+        sHalfWidthMap.put('\u3079', "\uFF8D\uFF9E");
+        sHalfWidthMap.put('\u307A', "\uFF8D\uFF9F");
+        sHalfWidthMap.put('\u307B', "\uFF8E");
+        sHalfWidthMap.put('\u307C', "\uFF8E\uFF9E");
+        sHalfWidthMap.put('\u307D', "\uFF8E\uFF9F");
+        sHalfWidthMap.put('\u307E', "\uFF8F");
+        sHalfWidthMap.put('\u307F', "\uFF90");
+        sHalfWidthMap.put('\u3080', "\uFF91");
+        sHalfWidthMap.put('\u3081', "\uFF92");
+        sHalfWidthMap.put('\u3082', "\uFF93");
+        sHalfWidthMap.put('\u3083', "\uFF6C");
+        sHalfWidthMap.put('\u3084', "\uFF94");
+        sHalfWidthMap.put('\u3085', "\uFF6D");
+        sHalfWidthMap.put('\u3086', "\uFF95");
+        sHalfWidthMap.put('\u3087', "\uFF6E");
+        sHalfWidthMap.put('\u3088', "\uFF96");
+        sHalfWidthMap.put('\u3089', "\uFF97");
+        sHalfWidthMap.put('\u308A', "\uFF98");
+        sHalfWidthMap.put('\u308B', "\uFF99");
+        sHalfWidthMap.put('\u308C', "\uFF9A");
+        sHalfWidthMap.put('\u308D', "\uFF9B");
+        sHalfWidthMap.put('\u308E', "\uFF9C");
+        sHalfWidthMap.put('\u308F', "\uFF9C");
+        sHalfWidthMap.put('\u3090', "\uFF72");
+        sHalfWidthMap.put('\u3091', "\uFF74");
+        sHalfWidthMap.put('\u3092', "\uFF66");
+        sHalfWidthMap.put('\u3093', "\uFF9D");
+        sHalfWidthMap.put('\u309B', "\uFF9E");
+        sHalfWidthMap.put('\u309C', "\uFF9F");
+        sHalfWidthMap.put('\u30A1', "\uFF67");
+        sHalfWidthMap.put('\u30A2', "\uFF71");
+        sHalfWidthMap.put('\u30A3', "\uFF68");
+        sHalfWidthMap.put('\u30A4', "\uFF72");
+        sHalfWidthMap.put('\u30A5', "\uFF69");
+        sHalfWidthMap.put('\u30A6', "\uFF73");
+        sHalfWidthMap.put('\u30A7', "\uFF6A");
+        sHalfWidthMap.put('\u30A8', "\uFF74");
+        sHalfWidthMap.put('\u30A9', "\uFF6B");
+        sHalfWidthMap.put('\u30AA', "\uFF75");
+        sHalfWidthMap.put('\u30AB', "\uFF76");
+        sHalfWidthMap.put('\u30AC', "\uFF76\uFF9E");
+        sHalfWidthMap.put('\u30AD', "\uFF77");
+        sHalfWidthMap.put('\u30AE', "\uFF77\uFF9E");
+        sHalfWidthMap.put('\u30AF', "\uFF78");
+        sHalfWidthMap.put('\u30B0', "\uFF78\uFF9E");
+        sHalfWidthMap.put('\u30B1', "\uFF79");
+        sHalfWidthMap.put('\u30B2', "\uFF79\uFF9E");
+        sHalfWidthMap.put('\u30B3', "\uFF7A");
+        sHalfWidthMap.put('\u30B4', "\uFF7A\uFF9E");
+        sHalfWidthMap.put('\u30B5', "\uFF7B");
+        sHalfWidthMap.put('\u30B6', "\uFF7B\uFF9E");
+        sHalfWidthMap.put('\u30B7', "\uFF7C");
+        sHalfWidthMap.put('\u30B8', "\uFF7C\uFF9E");
+        sHalfWidthMap.put('\u30B9', "\uFF7D");
+        sHalfWidthMap.put('\u30BA', "\uFF7D\uFF9E");
+        sHalfWidthMap.put('\u30BB', "\uFF7E");
+        sHalfWidthMap.put('\u30BC', "\uFF7E\uFF9E");
+        sHalfWidthMap.put('\u30BD', "\uFF7F");
+        sHalfWidthMap.put('\u30BE', "\uFF7F\uFF9E");
+        sHalfWidthMap.put('\u30BF', "\uFF80");
+        sHalfWidthMap.put('\u30C0', "\uFF80\uFF9E");
+        sHalfWidthMap.put('\u30C1', "\uFF81");
+        sHalfWidthMap.put('\u30C2', "\uFF81\uFF9E");
+        sHalfWidthMap.put('\u30C3', "\uFF6F");
+        sHalfWidthMap.put('\u30C4', "\uFF82");
+        sHalfWidthMap.put('\u30C5', "\uFF82\uFF9E");
+        sHalfWidthMap.put('\u30C6', "\uFF83");
+        sHalfWidthMap.put('\u30C7', "\uFF83\uFF9E");
+        sHalfWidthMap.put('\u30C8', "\uFF84");
+        sHalfWidthMap.put('\u30C9', "\uFF84\uFF9E");
+        sHalfWidthMap.put('\u30CA', "\uFF85");
+        sHalfWidthMap.put('\u30CB', "\uFF86");
+        sHalfWidthMap.put('\u30CC', "\uFF87");
+        sHalfWidthMap.put('\u30CD', "\uFF88");
+        sHalfWidthMap.put('\u30CE', "\uFF89");
+        sHalfWidthMap.put('\u30CF', "\uFF8A");
+        sHalfWidthMap.put('\u30D0', "\uFF8A\uFF9E");
+        sHalfWidthMap.put('\u30D1', "\uFF8A\uFF9F");
+        sHalfWidthMap.put('\u30D2', "\uFF8B");
+        sHalfWidthMap.put('\u30D3', "\uFF8B\uFF9E");
+        sHalfWidthMap.put('\u30D4', "\uFF8B\uFF9F");
+        sHalfWidthMap.put('\u30D5', "\uFF8C");
+        sHalfWidthMap.put('\u30D6', "\uFF8C\uFF9E");
+        sHalfWidthMap.put('\u30D7', "\uFF8C\uFF9F");
+        sHalfWidthMap.put('\u30D8', "\uFF8D");
+        sHalfWidthMap.put('\u30D9', "\uFF8D\uFF9E");
+        sHalfWidthMap.put('\u30DA', "\uFF8D\uFF9F");
+        sHalfWidthMap.put('\u30DB', "\uFF8E");
+        sHalfWidthMap.put('\u30DC', "\uFF8E\uFF9E");
+        sHalfWidthMap.put('\u30DD', "\uFF8E\uFF9F");
+        sHalfWidthMap.put('\u30DE', "\uFF8F");
+        sHalfWidthMap.put('\u30DF', "\uFF90");
+        sHalfWidthMap.put('\u30E0', "\uFF91");
+        sHalfWidthMap.put('\u30E1', "\uFF92");
+        sHalfWidthMap.put('\u30E2', "\uFF93");
+        sHalfWidthMap.put('\u30E3', "\uFF6C");
+        sHalfWidthMap.put('\u30E4', "\uFF94");
+        sHalfWidthMap.put('\u30E5', "\uFF6D");
+        sHalfWidthMap.put('\u30E6', "\uFF95");
+        sHalfWidthMap.put('\u30E7', "\uFF6E");
+        sHalfWidthMap.put('\u30E8', "\uFF96");
+        sHalfWidthMap.put('\u30E9', "\uFF97");
+        sHalfWidthMap.put('\u30EA', "\uFF98");
+        sHalfWidthMap.put('\u30EB', "\uFF99");
+        sHalfWidthMap.put('\u30EC', "\uFF9A");
+        sHalfWidthMap.put('\u30ED', "\uFF9B");
+        sHalfWidthMap.put('\u30EE', "\uFF9C");
+        sHalfWidthMap.put('\u30EF', "\uFF9C");
+        sHalfWidthMap.put('\u30F0', "\uFF72");
+        sHalfWidthMap.put('\u30F1', "\uFF74");
+        sHalfWidthMap.put('\u30F2', "\uFF66");
+        sHalfWidthMap.put('\u30F3', "\uFF9D");
+        sHalfWidthMap.put('\u30F4', "\uFF73\uFF9E");
+        sHalfWidthMap.put('\u30F5', "\uFF76");
+        sHalfWidthMap.put('\u30F6', "\uFF79");
+        sHalfWidthMap.put('\u30FB', "\uFF65");
+        sHalfWidthMap.put('\u30FC', "\uFF70");
+        sHalfWidthMap.put('\uFF01', "!");
+        sHalfWidthMap.put('\uFF02', "\"");
+        sHalfWidthMap.put('\uFF03', "#");
+        sHalfWidthMap.put('\uFF04', "$");
+        sHalfWidthMap.put('\uFF05', "%");
+        sHalfWidthMap.put('\uFF06', "&");
+        sHalfWidthMap.put('\uFF07', "'");
+        sHalfWidthMap.put('\uFF08', "(");
+        sHalfWidthMap.put('\uFF09', ")");
+        sHalfWidthMap.put('\uFF0A', "*");
+        sHalfWidthMap.put('\uFF0B', "+");
+        sHalfWidthMap.put('\uFF0C', ",");
+        sHalfWidthMap.put('\uFF0D', "-");
+        sHalfWidthMap.put('\uFF0E', ".");
+        sHalfWidthMap.put('\uFF0F', "/");
+        sHalfWidthMap.put('\uFF10', "0");
+        sHalfWidthMap.put('\uFF11', "1");
+        sHalfWidthMap.put('\uFF12', "2");
+        sHalfWidthMap.put('\uFF13', "3");
+        sHalfWidthMap.put('\uFF14', "4");
+        sHalfWidthMap.put('\uFF15', "5");
+        sHalfWidthMap.put('\uFF16', "6");
+        sHalfWidthMap.put('\uFF17', "7");
+        sHalfWidthMap.put('\uFF18', "8");
+        sHalfWidthMap.put('\uFF19', "9");
+        sHalfWidthMap.put('\uFF1A', ":");
+        sHalfWidthMap.put('\uFF1B', ";");
+        sHalfWidthMap.put('\uFF1C', "<");
+        sHalfWidthMap.put('\uFF1D', "=");
+        sHalfWidthMap.put('\uFF1E', ">");
+        sHalfWidthMap.put('\uFF1F', "?");
+        sHalfWidthMap.put('\uFF20', "@");
+        sHalfWidthMap.put('\uFF21', "A");
+        sHalfWidthMap.put('\uFF22', "B");
+        sHalfWidthMap.put('\uFF23', "C");
+        sHalfWidthMap.put('\uFF24', "D");
+        sHalfWidthMap.put('\uFF25', "E");
+        sHalfWidthMap.put('\uFF26', "F");
+        sHalfWidthMap.put('\uFF27', "G");
+        sHalfWidthMap.put('\uFF28', "H");
+        sHalfWidthMap.put('\uFF29', "I");
+        sHalfWidthMap.put('\uFF2A', "J");
+        sHalfWidthMap.put('\uFF2B', "K");
+        sHalfWidthMap.put('\uFF2C', "L");
+        sHalfWidthMap.put('\uFF2D', "M");
+        sHalfWidthMap.put('\uFF2E', "N");
+        sHalfWidthMap.put('\uFF2F', "O");
+        sHalfWidthMap.put('\uFF30', "P");
+        sHalfWidthMap.put('\uFF31', "Q");
+        sHalfWidthMap.put('\uFF32', "R");
+        sHalfWidthMap.put('\uFF33', "S");
+        sHalfWidthMap.put('\uFF34', "T");
+        sHalfWidthMap.put('\uFF35', "U");
+        sHalfWidthMap.put('\uFF36', "V");
+        sHalfWidthMap.put('\uFF37', "W");
+        sHalfWidthMap.put('\uFF38', "X");
+        sHalfWidthMap.put('\uFF39', "Y");
+        sHalfWidthMap.put('\uFF3A', "Z");
+        sHalfWidthMap.put('\uFF3B', "[");
+        sHalfWidthMap.put('\uFF3C', "\\");
+        sHalfWidthMap.put('\uFF3D', "]");
+        sHalfWidthMap.put('\uFF3E', "^");
+        sHalfWidthMap.put('\uFF3F', "_");
+        sHalfWidthMap.put('\uFF41', "a");
+        sHalfWidthMap.put('\uFF42', "b");
+        sHalfWidthMap.put('\uFF43', "c");
+        sHalfWidthMap.put('\uFF44', "d");
+        sHalfWidthMap.put('\uFF45', "e");
+        sHalfWidthMap.put('\uFF46', "f");
+        sHalfWidthMap.put('\uFF47', "g");
+        sHalfWidthMap.put('\uFF48', "h");
+        sHalfWidthMap.put('\uFF49', "i");
+        sHalfWidthMap.put('\uFF4A', "j");
+        sHalfWidthMap.put('\uFF4B', "k");
+        sHalfWidthMap.put('\uFF4C', "l");
+        sHalfWidthMap.put('\uFF4D', "m");
+        sHalfWidthMap.put('\uFF4E', "n");
+        sHalfWidthMap.put('\uFF4F', "o");
+        sHalfWidthMap.put('\uFF50', "p");
+        sHalfWidthMap.put('\uFF51', "q");
+        sHalfWidthMap.put('\uFF52', "r");
+        sHalfWidthMap.put('\uFF53', "s");
+        sHalfWidthMap.put('\uFF54', "t");
+        sHalfWidthMap.put('\uFF55', "u");
+        sHalfWidthMap.put('\uFF56', "v");
+        sHalfWidthMap.put('\uFF57', "w");
+        sHalfWidthMap.put('\uFF58', "x");
+        sHalfWidthMap.put('\uFF59', "y");
+        sHalfWidthMap.put('\uFF5A', "z");
+        sHalfWidthMap.put('\uFF5B', "{");
+        sHalfWidthMap.put('\uFF5C', "|");
+        sHalfWidthMap.put('\uFF5D', "}");
+        sHalfWidthMap.put('\uFF5E', "~");
+        sHalfWidthMap.put('\uFF61', "\uFF61");
+        sHalfWidthMap.put('\uFF62', "\uFF62");
+        sHalfWidthMap.put('\uFF63', "\uFF63");
+        sHalfWidthMap.put('\uFF64', "\uFF64");
+        sHalfWidthMap.put('\uFF65', "\uFF65");
+        sHalfWidthMap.put('\uFF66', "\uFF66");
+        sHalfWidthMap.put('\uFF67', "\uFF67");
+        sHalfWidthMap.put('\uFF68', "\uFF68");
+        sHalfWidthMap.put('\uFF69', "\uFF69");
+        sHalfWidthMap.put('\uFF6A', "\uFF6A");
+        sHalfWidthMap.put('\uFF6B', "\uFF6B");
+        sHalfWidthMap.put('\uFF6C', "\uFF6C");
+        sHalfWidthMap.put('\uFF6D', "\uFF6D");
+        sHalfWidthMap.put('\uFF6E', "\uFF6E");
+        sHalfWidthMap.put('\uFF6F', "\uFF6F");
+        sHalfWidthMap.put('\uFF70', "\uFF70");
+        sHalfWidthMap.put('\uFF71', "\uFF71");
+        sHalfWidthMap.put('\uFF72', "\uFF72");
+        sHalfWidthMap.put('\uFF73', "\uFF73");
+        sHalfWidthMap.put('\uFF74', "\uFF74");
+        sHalfWidthMap.put('\uFF75', "\uFF75");
+        sHalfWidthMap.put('\uFF76', "\uFF76");
+        sHalfWidthMap.put('\uFF77', "\uFF77");
+        sHalfWidthMap.put('\uFF78', "\uFF78");
+        sHalfWidthMap.put('\uFF79', "\uFF79");
+        sHalfWidthMap.put('\uFF7A', "\uFF7A");
+        sHalfWidthMap.put('\uFF7B', "\uFF7B");
+        sHalfWidthMap.put('\uFF7C', "\uFF7C");
+        sHalfWidthMap.put('\uFF7D', "\uFF7D");
+        sHalfWidthMap.put('\uFF7E', "\uFF7E");
+        sHalfWidthMap.put('\uFF7F', "\uFF7F");
+        sHalfWidthMap.put('\uFF80', "\uFF80");
+        sHalfWidthMap.put('\uFF81', "\uFF81");
+        sHalfWidthMap.put('\uFF82', "\uFF82");
+        sHalfWidthMap.put('\uFF83', "\uFF83");
+        sHalfWidthMap.put('\uFF84', "\uFF84");
+        sHalfWidthMap.put('\uFF85', "\uFF85");
+        sHalfWidthMap.put('\uFF86', "\uFF86");
+        sHalfWidthMap.put('\uFF87', "\uFF87");
+        sHalfWidthMap.put('\uFF88', "\uFF88");
+        sHalfWidthMap.put('\uFF89', "\uFF89");
+        sHalfWidthMap.put('\uFF8A', "\uFF8A");
+        sHalfWidthMap.put('\uFF8B', "\uFF8B");
+        sHalfWidthMap.put('\uFF8C', "\uFF8C");
+        sHalfWidthMap.put('\uFF8D', "\uFF8D");
+        sHalfWidthMap.put('\uFF8E', "\uFF8E");
+        sHalfWidthMap.put('\uFF8F', "\uFF8F");
+        sHalfWidthMap.put('\uFF90', "\uFF90");
+        sHalfWidthMap.put('\uFF91', "\uFF91");
+        sHalfWidthMap.put('\uFF92', "\uFF92");
+        sHalfWidthMap.put('\uFF93', "\uFF93");
+        sHalfWidthMap.put('\uFF94', "\uFF94");
+        sHalfWidthMap.put('\uFF95', "\uFF95");
+        sHalfWidthMap.put('\uFF96', "\uFF96");
+        sHalfWidthMap.put('\uFF97', "\uFF97");
+        sHalfWidthMap.put('\uFF98', "\uFF98");
+        sHalfWidthMap.put('\uFF99', "\uFF99");
+        sHalfWidthMap.put('\uFF9A', "\uFF9A");
+        sHalfWidthMap.put('\uFF9B', "\uFF9B");
+        sHalfWidthMap.put('\uFF9C', "\uFF9C");
+        sHalfWidthMap.put('\uFF9D', "\uFF9D");
+        sHalfWidthMap.put('\uFF9E', "\uFF9E");
+        sHalfWidthMap.put('\uFF9F', "\uFF9F");
+        sHalfWidthMap.put('\uFFE5', "\u005C\u005C");
+    }
+
+    /**
+     * 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(final char ch) {
+        if (sHalfWidthMap.containsKey(ch)) {
+            return sHalfWidthMap.get(ch);
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/vcard/VCardBuilder.java b/src/com/android/vcard/VCardBuilder.java
new file mode 100644 (file)
index 0000000..c16aad7
--- /dev/null
@@ -0,0 +1,2208 @@
+/*
+ * 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 com.android.vcard;
+
+import android.content.ContentValues;
+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;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Base64;
+//import android.util.CharsetUtils;
+import android.util.Log;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <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";
+
+    // If you add the other element, please check all the columns are able to be
+    // converted to String.
+    //
+    // e.g. BLOB is not what we can handle here now.
+    private static final Set<String> sAllowedAndroidPropertySet =
+            Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
+                    Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE,
+                    Relation.CONTENT_ITEM_TYPE)));
+
+    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;
+
+    private static final String VCARD_DATA_VCARD = "VCARD";
+    private static final String VCARD_DATA_PUBLIC = "PUBLIC";
+
+    private static final String VCARD_PARAM_SEPARATOR = ";";
+    private static final String VCARD_END_OF_LINE = "\r\n";
+    private static final String VCARD_DATA_SEPARATOR = ":";
+    private static final String VCARD_ITEM_SEPARATOR = ";";
+    private static final String VCARD_WS = " ";
+    private static final String VCARD_PARAM_EQUAL = "=";
+
+    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 final int mVCardType;
+
+    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 mAppendTypeParamName;
+    private final boolean mRefrainsQPToNameProperties;
+    private final boolean mNeedsToConvertPhoneticString;
+
+    private final boolean mShouldAppendCharsetParam;
+
+    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;
+
+        if (VCardConfig.isVersion40(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);
+        mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType);
+        mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType);
+        mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType);
+
+        // vCard 2.1 requires charset.
+        // vCard 3.0 does not allow it but we found some devices use it to determine
+        // the exact charset.
+        // We currently append it only when charset other than UTF_8 is used.
+        mShouldAppendCharsetParam =
+                !(VCardConfig.isVersion30(vcardType) && "UTF-8".equalsIgnoreCase(charset));
+
+        if (VCardConfig.isDoCoMo(vcardType)) {
+            if (!SHIFT_JIS.equalsIgnoreCase(charset)) {
+                /* Log.w(LOG_TAG,
+                        "The charset \"" + charset + "\" is used while "
+                        + SHIFT_JIS + " is needed to be used."); */
+                if (TextUtils.isEmpty(charset)) {
+                    mCharset = SHIFT_JIS;
+                } else {
+                    try {
+                        charset = CharsetUtils.charsetForVendor(charset).name();
+                    } catch (UnsupportedCharsetException e) {
+                        Log.i(LOG_TAG,
+                                "Career-specific \"" + charset + "\" was not found (as usual). "
+                                + "Use it as is.");
+                    }
+                    mCharset = charset;
+                }
+            } else {
+                if (mIsDoCoMo) {
+                    try {
+                        charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
+                    } catch (UnsupportedCharsetException e) {
+                        Log.e(LOG_TAG,
+                                "DoCoMo-specific SHIFT_JIS was not found. "
+                                + "Use SHIFT_JIS as is.");
+                        charset = SHIFT_JIS;
+                    }
+                } else {
+                    try {
+                        charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
+                    } catch (UnsupportedCharsetException e) {
+                        Log.e(LOG_TAG,
+                                "Career-specific SHIFT_JIS was not found. "
+                                + "Use SHIFT_JIS as is.");
+                        charset = SHIFT_JIS;
+                    }
+                }
+                mCharset = charset;
+            }
+            mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
+        } else {
+            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();
+    }
+
+    public void clear() {
+        mBuilder = new StringBuilder();
+        mEndAppended = false;
+        appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD);
+        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);
+        }
+    }
+
+    private boolean containsNonEmptyName(final ContentValues contentValues) {
+        final 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 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 displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
+        return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) &&
+                TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) &&
+                TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) &&
+                TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) &&
+                TextUtils.isEmpty(displayName));
+    }
+
+    private ContentValues getPrimaryContentValue(final List<ContentValues> contentValuesList) {
+        ContentValues primaryContentValues = null;
+        ContentValues subprimaryContentValues = null;
+        for (ContentValues contentValues : contentValuesList) {
+            if (contentValues == null){
+                continue;
+            }
+            Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY);
+            if (isSuperPrimary != null && isSuperPrimary > 0) {
+                // We choose "super primary" ContentValues.
+                primaryContentValues = contentValues;
+                break;
+            } else if (primaryContentValues == null) {
+                // We choose the first "primary" ContentValues
+                // if "super primary" ContentValues does not exist.
+                final Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY);
+                if (isPrimary != null && isPrimary > 0 &&
+                        containsNonEmptyName(contentValues)) {
+                    primaryContentValues = contentValues;
+                    // Do not break, since there may be ContentValues with "super primary"
+                    // afterword.
+                } else if (subprimaryContentValues == null &&
+                        containsNonEmptyName(contentValues)) {
+                    subprimaryContentValues = contentValues;
+                }
+            }
+        }
+
+        if (primaryContentValues == null) {
+            if (subprimaryContentValues != null) {
+                // We choose the first ContentValues if any "primary" ContentValues does not exist.
+                primaryContentValues = subprimaryContentValues;
+            } else {
+                Log.e(LOG_TAG, "All ContentValues given from database is empty.");
+                primaryContentValues = new ContentValues();
+            }
+        }
+
+        return primaryContentValues;
+    }
+
+    /**
+     * 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 (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;
+        }
+
+        final ContentValues contentValues = getPrimaryContentValue(contentValuesList);
+        final 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 displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
+
+        if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) {
+            final boolean reallyAppendCharsetParameterToName =
+                    shouldAppendCharsetParam(familyName, givenName, middleName, prefix, suffix);
+            final boolean reallyUseQuotedPrintableToName =
+                    (!mRefrainsQPToNameProperties &&
+                            !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) &&
+                                    VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) &&
+                                    VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) &&
+                                    VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) &&
+                                    VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix)));
+
+            final String formattedName;
+            if (!TextUtils.isEmpty(displayName)) {
+                formattedName = displayName;
+            } else {
+                formattedName = VCardUtils.constructNameFromElements(
+                        VCardConfig.getNameOrderType(mVCardType),
+                        familyName, middleName, givenName, prefix, suffix);
+            }
+            final boolean reallyAppendCharsetParameterToFN =
+                    shouldAppendCharsetParam(formattedName);
+            final boolean reallyUseQuotedPrintableToFN =
+                    !mRefrainsQPToNameProperties &&
+                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName);
+
+            final String encodedFamily;
+            final String encodedGiven;
+            final String encodedMiddle;
+            final String encodedPrefix;
+            final String encodedSuffix;
+            if (reallyUseQuotedPrintableToName) {
+                encodedFamily = encodeQuotedPrintable(familyName);
+                encodedGiven = encodeQuotedPrintable(givenName);
+                encodedMiddle = encodeQuotedPrintable(middleName);
+                encodedPrefix = encodeQuotedPrintable(prefix);
+                encodedSuffix = encodeQuotedPrintable(suffix);
+            } else {
+                encodedFamily = escapeCharacters(familyName);
+                encodedGiven = escapeCharacters(givenName);
+                encodedMiddle = escapeCharacters(middleName);
+                encodedPrefix = escapeCharacters(prefix);
+                encodedSuffix = escapeCharacters(suffix);
+            }
+
+            final String encodedFormattedname =
+                    (reallyUseQuotedPrintableToFN ?
+                            encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName));
+
+            mBuilder.append(VCardConstants.PROPERTY_N);
+            if (mIsDoCoMo) {
+                if (reallyAppendCharsetParameterToName) {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                    mBuilder.append(mVCardCharsetParameter);
+                }
+                if (reallyUseQuotedPrintableToName) {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                    mBuilder.append(VCARD_PARAM_ENCODING_QP);
+                }
+                mBuilder.append(VCARD_DATA_SEPARATOR);
+                // DoCoMo phones require that all the elements in the "family name" field.
+                mBuilder.append(formattedName);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+            } else {
+                if (reallyAppendCharsetParameterToName) {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                    mBuilder.append(mVCardCharsetParameter);
+                }
+                if (reallyUseQuotedPrintableToName) {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                    mBuilder.append(VCARD_PARAM_ENCODING_QP);
+                }
+                mBuilder.append(VCARD_DATA_SEPARATOR);
+                mBuilder.append(encodedFamily);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(encodedGiven);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(encodedMiddle);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(encodedPrefix);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(encodedSuffix);
+            }
+            mBuilder.append(VCARD_END_OF_LINE);
+
+            // FN property
+            mBuilder.append(VCardConstants.PROPERTY_FN);
+            if (reallyAppendCharsetParameterToFN) {
+                mBuilder.append(VCARD_PARAM_SEPARATOR);
+                mBuilder.append(mVCardCharsetParameter);
+            }
+            if (reallyUseQuotedPrintableToFN) {
+                mBuilder.append(VCARD_PARAM_SEPARATOR);
+                mBuilder.append(VCARD_PARAM_ENCODING_QP);
+            }
+            mBuilder.append(VCARD_DATA_SEPARATOR);
+            mBuilder.append(encodedFormattedname);
+            mBuilder.append(VCARD_END_OF_LINE);
+        } else if (!TextUtils.isEmpty(displayName)) {
+            final boolean reallyUseQuotedPrintableToDisplayName =
+                (!mRefrainsQPToNameProperties &&
+                        !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName));
+            final String encodedDisplayName =
+                    reallyUseQuotedPrintableToDisplayName ?
+                            encodeQuotedPrintable(displayName) :
+                                escapeCharacters(displayName);
+
+            // N
+            mBuilder.append(VCardConstants.PROPERTY_N);
+            if (shouldAppendCharsetParam(displayName)) {
+                mBuilder.append(VCARD_PARAM_SEPARATOR);
+                mBuilder.append(mVCardCharsetParameter);
+            }
+            if (reallyUseQuotedPrintableToDisplayName) {
+                mBuilder.append(VCARD_PARAM_SEPARATOR);
+                mBuilder.append(VCARD_PARAM_ENCODING_QP);
+            }
+            mBuilder.append(VCARD_DATA_SEPARATOR);
+            mBuilder.append(encodedDisplayName);
+            mBuilder.append(VCARD_ITEM_SEPARATOR);
+            mBuilder.append(VCARD_ITEM_SEPARATOR);
+            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 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);
+            }
+            mBuilder.append(VCARD_DATA_SEPARATOR);
+            mBuilder.append(encodedDisplayName);
+            mBuilder.append(VCARD_END_OF_LINE);
+        } else if (VCardConfig.isVersion30(mVCardType)) {
+            appendLine(VCardConstants.PROPERTY_N, "");
+            appendLine(VCardConstants.PROPERTY_FN, "");
+        } else if (mIsDoCoMo) {
+            appendLine(VCardConstants.PROPERTY_N, "");
+        }
+
+        appendPhoneticNameFields(contentValues);
+        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;
+        final String phoneticGivenName;
+        {
+            final String tmpPhoneticFamilyName =
+                contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
+            final String tmpPhoneticMiddleName =
+                contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
+            final String tmpPhoneticGivenName =
+                contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+            if (mNeedsToConvertPhoneticString) {
+                phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName);
+                phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName);
+                phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName);
+            } else {
+                phoneticFamilyName = tmpPhoneticFamilyName;
+                phoneticMiddleName = tmpPhoneticMiddleName;
+                phoneticGivenName = tmpPhoneticGivenName;
+            }
+        }
+
+        if (TextUtils.isEmpty(phoneticFamilyName)
+                && TextUtils.isEmpty(phoneticMiddleName)
+                && TextUtils.isEmpty(phoneticGivenName)) {
+            if (mIsDoCoMo) {
+                mBuilder.append(VCardConstants.PROPERTY_SOUND);
+                mBuilder.append(VCARD_PARAM_SEPARATOR);
+                mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
+                mBuilder.append(VCARD_DATA_SEPARATOR);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+                mBuilder.append(VCARD_END_OF_LINE);
+            }
+            return;
+        }
+
+        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 (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);
+            }
+            mBuilder.append(VCARD_DATA_SEPARATOR);
+            mBuilder.append(escapeCharacters(sortString));
+            mBuilder.append(VCARD_END_OF_LINE);
+        } else if (mIsJapaneseMobilePhone) {
+            // Note: There is no appropriate property for expressing
+            //       phonetic name (Yomigana in Japanese) in vCard 2.1, while there is in
+            //       vCard 3.0 (SORT-STRING).
+            //       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.
+            //       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);
+
+            boolean reallyUseQuotedPrintable =
+                (!mRefrainsQPToNameProperties
+                        && !(VCardUtils.containsOnlyNonCrLfPrintableAscii(
+                                phoneticFamilyName)
+                                && VCardUtils.containsOnlyNonCrLfPrintableAscii(
+                                        phoneticMiddleName)
+                                && VCardUtils.containsOnlyNonCrLfPrintableAscii(
+                                        phoneticGivenName)));
+
+            final String encodedPhoneticFamilyName;
+            final String encodedPhoneticMiddleName;
+            final String encodedPhoneticGivenName;
+            if (reallyUseQuotedPrintable) {
+                encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
+                encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
+                encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
+            } else {
+                encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
+                encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
+                encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
+            }
+
+            if (shouldAppendCharsetParam(encodedPhoneticFamilyName,
+                    encodedPhoneticMiddleName, encodedPhoneticGivenName)) {
+                mBuilder.append(VCARD_PARAM_SEPARATOR);
+                mBuilder.append(mVCardCharsetParameter);
+            }
+            mBuilder.append(VCARD_DATA_SEPARATOR);
+            {
+                boolean first = true;
+                if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) {
+                    mBuilder.append(encodedPhoneticFamilyName);
+                    first = false;
+                }
+                if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        mBuilder.append(' ');
+                    }
+                    mBuilder.append(encodedPhoneticMiddleName);
+                }
+                if (!TextUtils.isEmpty(encodedPhoneticGivenName)) {
+                    if (!first) {
+                        mBuilder.append(' ');
+                    }
+                    mBuilder.append(encodedPhoneticGivenName);
+                }
+            }
+            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);
+        }
+
+        if (mUsesDefactProperty) {
+            if (!TextUtils.isEmpty(phoneticGivenName)) {
+                final boolean reallyUseQuotedPrintable =
+                    (mShouldUseQuotedPrintable &&
+                            !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName));
+                final String encodedPhoneticGivenName;
+                if (reallyUseQuotedPrintable) {
+                    encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
+                } else {
+                    encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
+                }
+                mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME);
+                if (shouldAppendCharsetParam(phoneticGivenName)) {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                    mBuilder.append(mVCardCharsetParameter);
+                }
+                if (reallyUseQuotedPrintable) {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                    mBuilder.append(VCARD_PARAM_ENCODING_QP);
+                }
+                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 &&
+                            !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName));
+                final String encodedPhoneticMiddleName;
+                if (reallyUseQuotedPrintable) {
+                    encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
+                } else {
+                    encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
+                }
+                mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME);
+                if (shouldAppendCharsetParam(phoneticMiddleName)) {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                    mBuilder.append(mVCardCharsetParameter);
+                }
+                if (reallyUseQuotedPrintable) {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                    mBuilder.append(VCARD_PARAM_ENCODING_QP);
+                }
+                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 &&
+                            !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName));
+                final String encodedPhoneticFamilyName;
+                if (reallyUseQuotedPrintable) {
+                    encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
+                } else {
+                    encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
+                }
+                mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME);
+                if (shouldAppendCharsetParam(phoneticFamilyName)) {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                    mBuilder.append(mVCardCharsetParameter);
+                }
+                if (reallyUseQuotedPrintable) {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                    mBuilder.append(VCARD_PARAM_ENCODING_QP);
+                }
+                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 (mIsV30OrV40) {   // These specifications have NICKNAME property.
+            useAndroidProperty = false;
+        } else if (mUsesAndroidProperty) {
+            useAndroidProperty = true;
+        } else {
+            // There's no way to add this field.
+            return this;
+        }
+        if (contentValuesList != null) {
+            for (ContentValues contentValues : contentValuesList) {
+                final String nickname = contentValues.getAsString(Nickname.NAME);
+                if (TextUtils.isEmpty(nickname)) {
+                    continue;
+                }
+                if (useAndroidProperty) {
+                    appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues);
+                } else {
+                    appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname);
+                }
+            }
+        }
+        return this;
+    }
+
+    public VCardBuilder appendPhones(final List<ContentValues> contentValuesList) {
+        boolean phoneLineExists = false;
+        if (contentValuesList != null) {
+            Set<String> phoneSet = new HashSet<String>();
+            for (ContentValues contentValues : contentValuesList) {
+                final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE);
+                final String label = contentValues.getAsString(Phone.LABEL);
+                final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY);
+                final boolean isPrimary = (isPrimaryAsInteger != null ?
+                        (isPrimaryAsInteger > 0) : false);
+                String phoneNumber = contentValues.getAsString(Phone.NUMBER);
+                if (phoneNumber != null) {
+                    phoneNumber = phoneNumber.trim();
+                }
+                if (TextUtils.isEmpty(phoneNumber)) {
+                    continue;
+                }
+
+                // PAGER number needs unformatted "phone number".
+                // TODO: It would be better to have this logic as optional. 
+                final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE);
+                if (type == Phone.TYPE_PAGER ||
+                        VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
+                    phoneLineExists = true;
+                    if (!phoneSet.contains(phoneNumber)) {
+                        phoneSet.add(phoneNumber);
+                        appendTelLine(type, label, phoneNumber, isPrimary);
+                    }
+                } else {
+                    final List<String> phoneNumberList = splitAndTrimPhoneNumbers(phoneNumber);
+                    if (phoneNumberList.isEmpty()) {
+                        continue;
+                    }
+                    phoneLineExists = true;
+                    for (String actualPhoneNumber : phoneNumberList) {
+                        if (!phoneSet.contains(actualPhoneNumber)) {
+                            final int phoneFormat = VCardUtils.getPhoneNumberFormat(mVCardType);
+                            String formatted = 
+                                    PhoneNumberUtils.formatNumber(actualPhoneNumber);
+
+                            // In vCard 4.0, value type must be "a single URI value",
+                            // not just a phone number. (Based on vCard 4.0 rev.13)
+                            if (VCardConfig.isVersion40(mVCardType)
+                                    && !TextUtils.isEmpty(formatted)
+                                    && !formatted.startsWith("tel:")) {
+                                formatted = "tel:" + formatted;
+                            }
+
+                            // Pre-formatted string should be stored.
+                            phoneSet.add(actualPhoneNumber);
+                            appendTelLine(type, label, formatted, isPrimary);
+                        }
+                    }  // for (String actualPhoneNumber : phoneNumberList) {
+
+                    // TODO: TEL with SIP URI?
+                }
+            }
+        }
+
+        if (!phoneLineExists && mIsDoCoMo) {
+            appendTelLine(Phone.TYPE_HOME, "", "", false);
+        }
+
+        return this;
+    }
+
+    /**
+     * <p>
+     * Splits a given string expressing phone numbers into several strings, and remove
+     * unnecessary characters inside them. The size of a returned list becomes 1 when
+     * no split is needed.
+     * </p>
+     * <p>
+     * The given number "may" have several phone numbers when the contact entry is corrupted
+     * because of its original source.
+     * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)"
+     * </p>
+     * <p>
+     * This kind of "phone numbers" will not be created with Android vCard implementation,
+     * but we may encounter them if the source of the input data has already corrupted
+     * implementation.
+     * </p>
+     * <p>
+     * To handle this case, this method first splits its input into multiple parts
+     * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and
+     * removes unnecessary strings like "(Miami)".
+     * </p>
+     * <p>
+     * Do not call this method when trimming is inappropriate for its receivers.
+     * </p>
+     */
+    private List<String> splitAndTrimPhoneNumbers(final String phoneNumber) {
+        final List<String> phoneList = new ArrayList<String>();
+
+        StringBuilder builder = new StringBuilder();
+        final int length = phoneNumber.length();
+        for (int i = 0; i < length; i++) {
+            final char ch = phoneNumber.charAt(i);
+            if (Character.isDigit(ch) || ch == '+') {
+                builder.append(ch);
+            } else if ((ch == ';' || ch == '\n') && builder.length() > 0) {
+                phoneList.add(builder.toString());
+                builder = new StringBuilder();
+            }
+        }
+        if (builder.length() > 0) {
+            phoneList.add(builder.toString());
+        }
+
+        return phoneList;
+    }
+
+    public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) {
+        boolean emailAddressExists = false;
+        if (contentValuesList != null) {
+            final Set<String> addressSet = new HashSet<String>();
+            for (ContentValues contentValues : contentValuesList) {
+                String emailAddress = contentValues.getAsString(Email.DATA);
+                if (emailAddress != null) {
+                    emailAddress = emailAddress.trim();
+                }
+                if (TextUtils.isEmpty(emailAddress)) {
+                    continue;
+                }
+                Integer typeAsObject = contentValues.getAsInteger(Email.TYPE);
+                final int type = (typeAsObject != null ?
+                        typeAsObject : DEFAULT_EMAIL_TYPE);
+                final String label = contentValues.getAsString(Email.LABEL);
+                Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY);
+                final boolean isPrimary = (isPrimaryAsInteger != null ?
+                        (isPrimaryAsInteger > 0) : false);
+                emailAddressExists = true;
+                if (!addressSet.contains(emailAddress)) {
+                    addressSet.add(emailAddress);
+                    appendEmailLine(type, label, emailAddress, isPrimary);
+                }
+            }
+        }
+
+        if (!emailAddressExists && mIsDoCoMo) {
+            appendEmailLine(Email.TYPE_HOME, "", "", false);
+        }
+
+        return this;
+    }
+
+    public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) {
+        if (contentValuesList == null || contentValuesList.isEmpty()) {
+            if (mIsDoCoMo) {
+                mBuilder.append(VCardConstants.PROPERTY_ADR);
+                mBuilder.append(VCARD_PARAM_SEPARATOR);
+                mBuilder.append(VCardConstants.PARAM_TYPE_HOME);
+                mBuilder.append(VCARD_DATA_SEPARATOR);
+                mBuilder.append(VCARD_END_OF_LINE);
+            }
+        } else {
+            if (mIsDoCoMo) {
+                appendPostalsForDoCoMo(contentValuesList);
+            } else {
+                appendPostalsForGeneric(contentValuesList);
+            }
+        }
+
+        return this;
+    }
+
+    private static final Map<Integer, Integer> sPostalTypePriorityMap;
+
+    static {
+        sPostalTypePriorityMap = new HashMap<Integer, Integer>();
+        sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0);
+        sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1);
+        sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2);
+        sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3);
+    }
+
+    /**
+     * Tries to append just one line. If there's no appropriate address
+     * information, append an empty line.
+     */
+    private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) {
+        int currentPriority = Integer.MAX_VALUE;
+        int currentType = Integer.MAX_VALUE;
+        ContentValues currentContentValues = null;
+        for (final ContentValues contentValues : contentValuesList) {
+            if (contentValues == null) {
+                continue;
+            }
+            final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
+            final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger);
+            final int priority =
+                    (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE);
+            if (priority < currentPriority) {
+                currentPriority = priority;
+                currentType = typeAsInteger;
+                currentContentValues = contentValues;
+                if (priority == 0) {
+                    break;
+                }
+            }
+        }
+
+        if (currentContentValues == null) {
+            Log.w(LOG_TAG, "Should not come here. Must have at least one postal data.");
+            return;
+        }
+
+        final String label = currentContentValues.getAsString(StructuredPostal.LABEL);
+        appendPostalLine(currentType, label, currentContentValues, false, true);
+    }
+
+    private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) {
+        for (final ContentValues contentValues : contentValuesList) {
+            if (contentValues == null) {
+                continue;
+            }
+            final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
+            final int type = (typeAsInteger != null ?
+                    typeAsInteger : DEFAULT_POSTAL_TYPE);
+            final String label = contentValues.getAsString(StructuredPostal.LABEL);
+            final Integer isPrimaryAsInteger =
+                contentValues.getAsInteger(StructuredPostal.IS_PRIMARY);
+            final boolean isPrimary = (isPrimaryAsInteger != null ?
+                    (isPrimaryAsInteger > 0) : false);
+            appendPostalLine(type, label, contentValues, isPrimary, false);
+        }
+    }
+
+    private static class PostalStruct {
+        final boolean reallyUseQuotedPrintable;
+        final boolean appendCharset;
+        final String addressData;
+        public PostalStruct(final boolean reallyUseQuotedPrintable,
+                final boolean appendCharset, final String addressData) {
+            this.reallyUseQuotedPrintable = reallyUseQuotedPrintable;
+            this.appendCharset = appendCharset;
+            this.addressData = addressData;
+        }
+    }
+
+    /**
+     * @return null when there's no information available to construct the data.
+     */
+    private PostalStruct tryConstructPostalStruct(ContentValues contentValues) {
+        // adr-value    = 0*6(text-value ";") text-value
+        //              ; PO Box, Extended Address, Street, Locality, Region, Postal
+        //              ; Code, Country Name
+        final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX);
+        final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD);
+        final String rawStreet = contentValues.getAsString(StructuredPostal.STREET);
+        final String rawLocality = contentValues.getAsString(StructuredPostal.CITY);
+        final String rawRegion = contentValues.getAsString(StructuredPostal.REGION);
+        final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE);
+        final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY);
+        final String[] rawAddressArray = new String[]{
+                rawPoBox, rawNeighborhood, rawStreet, rawLocality,
+                rawRegion, rawPostalCode, rawCountry};
+        if (!VCardUtils.areAllEmpty(rawAddressArray)) {
+            final boolean reallyUseQuotedPrintable =
+                (mShouldUseQuotedPrintable &&
+                        !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray));
+            final boolean appendCharset =
+                !VCardUtils.containsOnlyPrintableAscii(rawAddressArray);
+            final String encodedPoBox;
+            final String encodedStreet;
+            final String encodedLocality;
+            final String encodedRegion;
+            final String encodedPostalCode;
+            final String encodedCountry;
+            final String encodedNeighborhood;
+
+            final String rawLocality2;
+            // This looks inefficient since we encode rawLocality and rawNeighborhood twice,
+            // but this is intentional.
+            //
+            // QP encoding may add line feeds when needed and the result of
+            // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood)
+            // may be different from
+            // - encodedLocality + " " + encodedNeighborhood.
+            //
+            // We use safer way.
+            if (TextUtils.isEmpty(rawLocality)) {
+                if (TextUtils.isEmpty(rawNeighborhood)) {
+                    rawLocality2 = "";
+                } else {
+                    rawLocality2 = rawNeighborhood;
+                }
+            } else {
+                if (TextUtils.isEmpty(rawNeighborhood)) {
+                    rawLocality2 = rawLocality;
+                } else {
+                    rawLocality2 = rawLocality + " " + rawNeighborhood;
+                }
+            }
+            if (reallyUseQuotedPrintable) {
+                encodedPoBox = encodeQuotedPrintable(rawPoBox);
+                encodedStreet = encodeQuotedPrintable(rawStreet);
+                encodedLocality = encodeQuotedPrintable(rawLocality2);
+                encodedRegion = encodeQuotedPrintable(rawRegion);
+                encodedPostalCode = encodeQuotedPrintable(rawPostalCode);
+                encodedCountry = encodeQuotedPrintable(rawCountry);
+            } else {
+                encodedPoBox = escapeCharacters(rawPoBox);
+                encodedStreet = escapeCharacters(rawStreet);
+                encodedLocality = escapeCharacters(rawLocality2);
+                encodedRegion = escapeCharacters(rawRegion);
+                encodedPostalCode = escapeCharacters(rawPostalCode);
+                encodedCountry = escapeCharacters(rawCountry);
+                encodedNeighborhood = escapeCharacters(rawNeighborhood);
+            }
+            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, addressBuilder.toString());
+        } else {  // VCardUtils.areAllEmpty(rawAddressArray) == true
+            // Try to use FORMATTED_ADDRESS instead.
+            final String rawFormattedAddress =
+                contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
+            if (TextUtils.isEmpty(rawFormattedAddress)) {
+                return null;
+            }
+            final boolean reallyUseQuotedPrintable =
+                (mShouldUseQuotedPrintable &&
+                        !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress));
+            final boolean appendCharset =
+                !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress);
+            final String encodedFormattedAddress;
+            if (reallyUseQuotedPrintable) {
+                encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress);
+            } else {
+                encodedFormattedAddress = escapeCharacters(rawFormattedAddress);
+            }
+
+            // 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 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, addressBuilder.toString());
+        }
+    }
+
+    public VCardBuilder appendIms(final List<ContentValues> contentValuesList) {
+        if (contentValuesList != null) {
+            for (ContentValues contentValues : contentValuesList) {
+                final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL);
+                if (protocolAsObject == null) {
+                    continue;
+                }
+                final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject);
+                if (propertyName == null) {
+                    continue;
+                }
+                String data = contentValues.getAsString(Im.DATA);
+                if (data != null) {
+                    data = data.trim();
+                }
+                if (TextUtils.isEmpty(data)) {
+                    continue;
+                }
+                final String typeAsString;
+                {
+                    final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE);
+                    switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) {
+                        case Im.TYPE_HOME: {
+                            typeAsString = VCardConstants.PARAM_TYPE_HOME;
+                            break;
+                        }
+                        case Im.TYPE_WORK: {
+                            typeAsString = VCardConstants.PARAM_TYPE_WORK;
+                            break;
+                        }
+                        case Im.TYPE_CUSTOM: {
+                            final String label = contentValues.getAsString(Im.LABEL);
+                            typeAsString = (label != null ? "X-" + label : null);
+                            break;
+                        }
+                        case Im.TYPE_OTHER:  // Ignore
+                        default: {
+                            typeAsString = null;
+                            break;
+                        }
+                    }
+                }
+
+                final List<String> parameterList = new ArrayList<String>();
+                if (!TextUtils.isEmpty(typeAsString)) {
+                    parameterList.add(typeAsString);
+                }
+                final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY);
+                final boolean isPrimary = (isPrimaryAsInteger != null ?
+                        (isPrimaryAsInteger > 0) : false);
+                if (isPrimary) {
+                    parameterList.add(VCardConstants.PARAM_TYPE_PREF);
+                }
+
+                appendLineWithCharsetAndQPDetection(propertyName, parameterList, data);
+            }
+        }
+        return this;
+    }
+
+    public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) {
+        if (contentValuesList != null) {
+            for (ContentValues contentValues : contentValuesList) {
+                String website = contentValues.getAsString(Website.URL);
+                if (website != null) {
+                    website = website.trim();
+                }
+
+                // Note: vCard 3.0 does not allow any parameter addition toward "URL"
+                //       property, while there's no document in vCard 2.1.
+                if (!TextUtils.isEmpty(website)) {
+                    appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website);
+                }
+            }
+        }
+        return this;
+    }
+
+    public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) {
+        if (contentValuesList != null) {
+            for (ContentValues contentValues : contentValuesList) {
+                String company = contentValues.getAsString(Organization.COMPANY);
+                if (company != null) {
+                    company = company.trim();
+                }
+                String department = contentValues.getAsString(Organization.DEPARTMENT);
+                if (department != null) {
+                    department = department.trim();
+                }
+                String title = contentValues.getAsString(Organization.TITLE);
+                if (title != null) {
+                    title = title.trim();
+                }
+
+                StringBuilder orgBuilder = new StringBuilder();
+                if (!TextUtils.isEmpty(company)) {
+                    orgBuilder.append(company);
+                }
+                if (!TextUtils.isEmpty(department)) {
+                    if (orgBuilder.length() > 0) {
+                        orgBuilder.append(';');
+                    }
+                    orgBuilder.append(department);
+                }
+                final String orgline = orgBuilder.toString();
+                appendLine(VCardConstants.PROPERTY_ORG, orgline,
+                        !VCardUtils.containsOnlyPrintableAscii(orgline),
+                        (mShouldUseQuotedPrintable &&
+                                !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline)));
+
+                if (!TextUtils.isEmpty(title)) {
+                    appendLine(VCardConstants.PROPERTY_TITLE, title,
+                            !VCardUtils.containsOnlyPrintableAscii(title),
+                            (mShouldUseQuotedPrintable &&
+                                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(title)));
+                }
+            }
+        }
+        return this;
+    }
+
+    public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) {
+        if (contentValuesList != null) {
+            for (ContentValues contentValues : contentValuesList) {
+                if (contentValues == null) {
+                    continue;
+                }
+                byte[] data = contentValues.getAsByteArray(Photo.PHOTO);
+                if (data == null) {
+                    continue;
+                }
+                final String photoType = VCardUtils.guessImageType(data);
+                if (photoType == null) {
+                    Log.d(LOG_TAG, "Unknown photo type. Ignored.");
+                    continue;
+                }
+                // TODO: check this works fine.
+                final String photoString = new String(Base64.encode(data, Base64.NO_WRAP));
+                if (!TextUtils.isEmpty(photoString)) {
+                    appendPhotoLine(photoString, photoType);
+                }
+            }
+        }
+        return this;
+    }
+
+    public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) {
+        if (contentValuesList != null) {
+            if (mOnlyOneNoteFieldIsAvailable) {
+                final StringBuilder noteBuilder = new StringBuilder();
+                boolean first = true;
+                for (final ContentValues contentValues : contentValuesList) {
+                    String note = contentValues.getAsString(Note.NOTE);
+                    if (note == null) {
+                        note = "";
+                    }
+                    if (note.length() > 0) {
+                        if (first) {
+                            first = false;
+                        } else {
+                            noteBuilder.append('\n');
+                        }
+                        noteBuilder.append(note);
+                    }
+                }
+                final String noteStr = noteBuilder.toString();
+                // This means we scan noteStr completely twice, which is redundant.
+                // But for now, we assume this is not so time-consuming..
+                final boolean shouldAppendCharsetInfo =
+                    !VCardUtils.containsOnlyPrintableAscii(noteStr);
+                final boolean reallyUseQuotedPrintable =
+                        (mShouldUseQuotedPrintable &&
+                            !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
+                appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
+                        shouldAppendCharsetInfo, reallyUseQuotedPrintable);
+            } else {
+                for (ContentValues contentValues : contentValuesList) {
+                    final String noteStr = contentValues.getAsString(Note.NOTE);
+                    if (!TextUtils.isEmpty(noteStr)) {
+                        final boolean shouldAppendCharsetInfo =
+                                !VCardUtils.containsOnlyPrintableAscii(noteStr);
+                        final boolean reallyUseQuotedPrintable =
+                                (mShouldUseQuotedPrintable &&
+                                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
+                        appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
+                                shouldAppendCharsetInfo, reallyUseQuotedPrintable);
+                    }
+                }
+            }
+        }
+        return this;
+    }
+
+    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;
+            for (final ContentValues contentValues : contentValuesList) {
+                if (contentValues == null) {
+                    continue;
+                }
+                final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE);
+                final int eventType;
+                if (eventTypeAsInteger != null) {
+                    eventType = eventTypeAsInteger;
+                } else {
+                    eventType = Event.TYPE_OTHER;
+                }
+                if (eventType == Event.TYPE_BIRTHDAY) {
+                    final String birthdayCandidate = contentValues.getAsString(Event.START_DATE);
+                    if (birthdayCandidate == null) {
+                        continue;
+                    }
+                    final Integer isSuperPrimaryAsInteger =
+                        contentValues.getAsInteger(Event.IS_SUPER_PRIMARY);
+                    final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ?
+                            (isSuperPrimaryAsInteger > 0) : false);
+                    if (isSuperPrimary) {
+                        // "super primary" birthday should the prefered one.
+                        primaryBirthday = birthdayCandidate;
+                        break;
+                    }
+                    final Integer isPrimaryAsInteger =
+                        contentValues.getAsInteger(Event.IS_PRIMARY);
+                    final boolean isPrimary = (isPrimaryAsInteger != null ?
+                            (isPrimaryAsInteger > 0) : false);
+                    if (isPrimary) {
+                        // We don't break here since "super primary" birthday may exist later.
+                        primaryBirthday = birthdayCandidate;
+                    } else if (secondaryBirthday == null) {
+                        // First entry is set to the "secondary" candidate.
+                        secondaryBirthday = birthdayCandidate;
+                    }
+                } else if (mUsesAndroidProperty) {
+                    // Event types other than Birthday is not supported by vCard.
+                    appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues);
+                }
+            }
+            if (primaryBirthday != null) {
+                appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
+                        primaryBirthday.trim());
+            } else if (secondaryBirthday != null){
+                appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
+                        secondaryBirthday.trim());
+            }
+        }
+        return this;
+    }
+
+    public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) {
+        if (mUsesAndroidProperty && contentValuesList != null) {
+            for (final ContentValues contentValues : contentValuesList) {
+                if (contentValues == null) {
+                    continue;
+                }
+                appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues);
+            }
+        }
+        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 emitEveryTime) {
+        final boolean reallyUseQuotedPrintable;
+        final boolean appendCharset;
+        final String addressValue;
+        {
+            PostalStruct postalStruct = tryConstructPostalStruct(contentValues);
+            if (postalStruct == null) {
+                if (emitEveryTime) {
+                    reallyUseQuotedPrintable = false;
+                    appendCharset = false;
+                    addressValue = "";
+                } else {
+                    return;
+                }
+            } else {
+                reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable;
+                appendCharset = postalStruct.appendCharset;
+                addressValue = postalStruct.addressData;
+            }
+        }
+
+        List<String> parameterList = new ArrayList<String>();
+        if (isPrimary) {
+            parameterList.add(VCardConstants.PARAM_TYPE_PREF);
+        }
+        switch (type) {
+            case StructuredPostal.TYPE_HOME: {
+                parameterList.add(VCardConstants.PARAM_TYPE_HOME);
+                break;
+            }
+            case StructuredPostal.TYPE_WORK: {
+                parameterList.add(VCardConstants.PARAM_TYPE_WORK);
+                break;
+            }
+            case StructuredPostal.TYPE_CUSTOM: {
+                if (!TextUtils.isEmpty(label)
+                        && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
+                    // We're not sure whether the label is valid in the spec
+                    // ("IANA-token" in the vCard 3.0 is unclear...)
+                    // Just  for safety, we add "X-" at the beggining of each label.
+                    // Also checks the label obeys with vCard 3.0 spec.
+                    parameterList.add("X-" + label);
+                }
+                break;
+            }
+            case StructuredPostal.TYPE_OTHER: {
+                break;
+            }
+            default: {
+                Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type);
+                break;
+            }
+        }
+
+        mBuilder.append(VCardConstants.PROPERTY_ADR);
+        if (!parameterList.isEmpty()) {
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            appendTypeParameters(parameterList);
+        }
+        if (appendCharset) {
+            // Strictly, vCard 3.0 does not allow exporters to emit charset information,
+            // but we will add it since the information should be useful for importers,
+            //
+            // Assume no parser does not emit error with this parameter in vCard 3.0.
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            mBuilder.append(mVCardCharsetParameter);
+        }
+        if (reallyUseQuotedPrintable) {
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            mBuilder.append(VCARD_PARAM_ENCODING_QP);
+        }
+        mBuilder.append(VCARD_DATA_SEPARATOR);
+        mBuilder.append(addressValue);
+        mBuilder.append(VCARD_END_OF_LINE);
+    }
+
+    public void appendEmailLine(final int type, final String label,
+            final String rawValue, final boolean isPrimary) {
+        final String typeAsString;
+        switch (type) {
+            case Email.TYPE_CUSTOM: {
+                if (VCardUtils.isMobilePhoneLabel(label)) {
+                    typeAsString = VCardConstants.PARAM_TYPE_CELL;
+                } else if (!TextUtils.isEmpty(label)
+                        && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
+                    typeAsString = "X-" + label;
+                } else {
+                    typeAsString = null;
+                }
+                break;
+            }
+            case Email.TYPE_HOME: {
+                typeAsString = VCardConstants.PARAM_TYPE_HOME;
+                break;
+            }
+            case Email.TYPE_WORK: {
+                typeAsString = VCardConstants.PARAM_TYPE_WORK;
+                break;
+            }
+            case Email.TYPE_OTHER: {
+                typeAsString = null;
+                break;
+            }
+            case Email.TYPE_MOBILE: {
+                typeAsString = VCardConstants.PARAM_TYPE_CELL;
+                break;
+            }
+            default: {
+                Log.e(LOG_TAG, "Unknown Email type: " + type);
+                typeAsString = null;
+                break;
+            }
+        }
+
+        final List<String> parameterList = new ArrayList<String>();
+        if (isPrimary) {
+            parameterList.add(VCardConstants.PARAM_TYPE_PREF);
+        }
+        if (!TextUtils.isEmpty(typeAsString)) {
+            parameterList.add(typeAsString);
+        }
+
+        appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList,
+                rawValue);
+    }
+
+    public void appendTelLine(final Integer typeAsInteger, final String label,
+            final String encodedValue, boolean isPrimary) {
+        mBuilder.append(VCardConstants.PROPERTY_TEL);
+        mBuilder.append(VCARD_PARAM_SEPARATOR);
+
+        final int type;
+        if (typeAsInteger == null) {
+            type = Phone.TYPE_OTHER;
+        } else {
+            type = typeAsInteger;
+        }
+
+        ArrayList<String> parameterList = new ArrayList<String>();
+        switch (type) {
+            case Phone.TYPE_HOME: {
+                parameterList.addAll(
+                        Arrays.asList(VCardConstants.PARAM_TYPE_HOME));
+                break;
+            }
+            case Phone.TYPE_WORK: {
+                parameterList.addAll(
+                        Arrays.asList(VCardConstants.PARAM_TYPE_WORK));
+                break;
+            }
+            case Phone.TYPE_FAX_HOME: {
+                parameterList.addAll(
+                        Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX));
+                break;
+            }
+            case Phone.TYPE_FAX_WORK: {
+                parameterList.addAll(
+                        Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX));
+                break;
+            }
+            case Phone.TYPE_MOBILE: {
+                parameterList.add(VCardConstants.PARAM_TYPE_CELL);
+                break;
+            }
+            case Phone.TYPE_PAGER: {
+                if (mIsDoCoMo) {
+                    // Not sure about the reason, but previous implementation had
+                    // used "VOICE" instead of "PAGER"
+                    parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
+                } else {
+                    parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
+                }
+                break;
+            }
+            case Phone.TYPE_OTHER: {
+                parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
+                break;
+            }
+            case Phone.TYPE_CAR: {
+                parameterList.add(VCardConstants.PARAM_TYPE_CAR);
+                break;
+            }
+            case Phone.TYPE_COMPANY_MAIN: {
+                // There's no relevant field in vCard (at least 2.1).
+                parameterList.add(VCardConstants.PARAM_TYPE_WORK);
+                isPrimary = true;
+                break;
+            }
+            case Phone.TYPE_ISDN: {
+                parameterList.add(VCardConstants.PARAM_TYPE_ISDN);
+                break;
+            }
+            case Phone.TYPE_MAIN: {
+                isPrimary = true;
+                break;
+            }
+            case Phone.TYPE_OTHER_FAX: {
+                parameterList.add(VCardConstants.PARAM_TYPE_FAX);
+                break;
+            }
+            case Phone.TYPE_TELEX: {
+                parameterList.add(VCardConstants.PARAM_TYPE_TLX);
+                break;
+            }
+            case Phone.TYPE_WORK_MOBILE: {
+                parameterList.addAll(
+                        Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL));
+                break;
+            }
+            case Phone.TYPE_WORK_PAGER: {
+                parameterList.add(VCardConstants.PARAM_TYPE_WORK);
+                // See above.
+                if (mIsDoCoMo) {
+                    parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
+                } else {
+                    parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
+                }
+                break;
+            }
+            case Phone.TYPE_MMS: {
+                parameterList.add(VCardConstants.PARAM_TYPE_MSG);
+                break;
+            }
+            case Phone.TYPE_CUSTOM: {
+                if (TextUtils.isEmpty(label)) {
+                    // Just ignore the custom type.
+                    parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
+                } else if (VCardUtils.isMobilePhoneLabel(label)) {
+                    parameterList.add(VCardConstants.PARAM_TYPE_CELL);
+                } else if (mIsV30OrV40) {
+                    // This label is appropriately encoded in appendTypeParameters.
+                    parameterList.add(label);
+                } else {
+                    final String upperLabel = label.toUpperCase();
+                    if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) {
+                        parameterList.add(upperLabel);
+                    } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
+                        // Note: Strictly, vCard 2.1 does not allow "X-" parameter without
+                        //       "TYPE=" string.
+                        parameterList.add("X-" + label);
+                    }
+                }
+                break;
+            }
+            case Phone.TYPE_RADIO:
+            case Phone.TYPE_TTY_TDD:
+            default: {
+                break;
+            }
+        }
+
+        if (isPrimary) {
+            parameterList.add(VCardConstants.PARAM_TYPE_PREF);
+        }
+
+        if (parameterList.isEmpty()) {
+            appendUncommonPhoneType(mBuilder, type);
+        } else {
+            appendTypeParameters(parameterList);
+        }
+
+        mBuilder.append(VCARD_DATA_SEPARATOR);
+        mBuilder.append(encodedValue);
+        mBuilder.append(VCARD_END_OF_LINE);
+    }
+
+    /**
+     * Appends phone type string which may not be available in some devices.
+     */
+    private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) {
+        if (mIsDoCoMo) {
+            // The previous implementation for DoCoMo had been conservative
+            // about miscellaneous types.
+            builder.append(VCardConstants.PARAM_TYPE_VOICE);
+        } else {
+            String phoneType = VCardUtils.getPhoneTypeString(type);
+            if (phoneType != null) {
+                appendTypeParameter(phoneType);
+            } else {
+                Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type);
+            }
+        }
+    }
+
+    /**
+     * @param encodedValue Must be encoded by BASE64 
+     * @param photoType
+     */
+    public void appendPhotoLine(final String encodedValue, final String photoType) {
+        StringBuilder tmpBuilder = new StringBuilder();
+        tmpBuilder.append(VCardConstants.PROPERTY_PHOTO);
+        tmpBuilder.append(VCARD_PARAM_SEPARATOR);
+        if (mIsV30OrV40) {
+            tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_AS_B);
+        } else {
+            tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21);
+        }
+        tmpBuilder.append(VCARD_PARAM_SEPARATOR);
+        appendTypeParameter(tmpBuilder, photoType);
+        tmpBuilder.append(VCARD_DATA_SEPARATOR);
+        tmpBuilder.append(encodedValue);
+
+        final String tmpStr = tmpBuilder.toString();
+        tmpBuilder = new StringBuilder();
+        int lineCount = 0;
+        final int length = tmpStr.length();
+        final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30
+                - VCARD_END_OF_LINE.length();
+        final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length();
+        int maxNum = maxNumForFirstLine;
+        for (int i = 0; i < length; i++) {
+            tmpBuilder.append(tmpStr.charAt(i));
+            lineCount++;
+            if (lineCount > maxNum) {
+                tmpBuilder.append(VCARD_END_OF_LINE);
+                tmpBuilder.append(VCARD_WS);
+                maxNum = maxNumInGeneral;
+                lineCount = 0;
+            }
+        }
+        mBuilder.append(tmpBuilder.toString());
+        mBuilder.append(VCARD_END_OF_LINE);
+        mBuilder.append(VCARD_END_OF_LINE);
+    }
+
+    /**
+     * SIP (Session Initiation Protocol) is first supported in RFC 4770 as part of IMPP
+     * support. vCard 2.1 and old vCard 3.0 may not able to parse it, or expect X-SIP
+     * instead of "IMPP;sip:...".
+     *
+     * We honor RFC 4770 and don't allow vCard 3.0 to emit X-SIP at all.
+     */
+    public VCardBuilder appendSipAddresses(final List<ContentValues> contentValuesList) {
+        final boolean useXProperty;
+        if (mIsV30OrV40) {
+            useXProperty = false;
+        } else if (mUsesDefactProperty){
+            useXProperty = true;
+        } else {
+            return this;
+        }
+
+        if (contentValuesList != null) {
+            for (ContentValues contentValues : contentValuesList) {
+                String sipAddress = contentValues.getAsString(SipAddress.SIP_ADDRESS);
+                if (TextUtils.isEmpty(sipAddress)) {
+                    continue;
+                }
+                if (useXProperty) {
+                    // X-SIP does not contain "sip:" prefix.
+                    if (sipAddress.startsWith("sip:")) {
+                        if (sipAddress.length() == 4) {
+                            continue;
+                        }
+                        sipAddress = sipAddress.substring(4);
+                    }
+                    // No type is available yet.
+                    appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_X_SIP, sipAddress);
+                } else {
+                    if (!sipAddress.startsWith("sip:")) {
+                        sipAddress = "sip:" + sipAddress;
+                    }
+                    final String propertyName;
+                    if (VCardConfig.isVersion40(mVCardType)) {
+                        // We have two ways to emit sip address: TEL and IMPP. Currently (rev.13)
+                        // TEL seems appropriate but may change in the future.
+                        propertyName = VCardConstants.PROPERTY_TEL;
+                    } else {
+                        // RFC 4770 (for vCard 3.0)
+                        propertyName = VCardConstants.PROPERTY_IMPP;
+                    }
+                    appendLineWithCharsetAndQPDetection(propertyName, sipAddress);
+                }
+            }
+        }
+        return this;
+    }
+
+    public void appendAndroidSpecificProperty(
+            final String mimeType, ContentValues contentValues) {
+        if (!sAllowedAndroidPropertySet.contains(mimeType)) {
+            return;
+        }
+        final List<String> rawValueList = new ArrayList<String>();
+        for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) {
+            String value = contentValues.getAsString("data" + i);
+            if (value == null) {
+                value = "";
+            }
+            rawValueList.add(value);
+        }
+
+        boolean needCharset =
+            (mShouldAppendCharsetParam &&
+                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
+        boolean reallyUseQuotedPrintable =
+            (mShouldUseQuotedPrintable &&
+                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
+        mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM);
+        if (needCharset) {
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            mBuilder.append(mVCardCharsetParameter);
+        }
+        if (reallyUseQuotedPrintable) {
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            mBuilder.append(VCARD_PARAM_ENCODING_QP);
+        }
+        mBuilder.append(VCARD_DATA_SEPARATOR);
+        mBuilder.append(mimeType);  // Should not be encoded.
+        for (String rawValue : rawValueList) {
+            final String encodedValue;
+            if (reallyUseQuotedPrintable) {
+                encodedValue = encodeQuotedPrintable(rawValue);
+            } else {
+                // TODO: one line may be too huge, which may be invalid in vCard 3.0
+                //        (which says "When generating a content line, lines longer than
+                //        75 characters SHOULD be folded"), though several
+                //        (even well-known) applications do not care this.
+                encodedValue = escapeCharacters(rawValue);
+            }
+            mBuilder.append(VCARD_ITEM_SEPARATOR);
+            mBuilder.append(encodedValue);
+        }
+        mBuilder.append(VCARD_END_OF_LINE);
+    }
+
+    public void appendLineWithCharsetAndQPDetection(final String propertyName,
+            final String rawValue) {
+        appendLineWithCharsetAndQPDetection(propertyName, null, rawValue);
+    }
+
+    public void appendLineWithCharsetAndQPDetection(
+            final String propertyName, final List<String> rawValueList) {
+        appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList);
+    }
+
+    public void appendLineWithCharsetAndQPDetection(final String propertyName,
+            final List<String> parameterList, final String rawValue) {
+        final boolean needCharset =
+                !VCardUtils.containsOnlyPrintableAscii(rawValue);
+        final boolean reallyUseQuotedPrintable =
+                (mShouldUseQuotedPrintable &&
+                        !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue));
+        appendLine(propertyName, parameterList,
+                rawValue, needCharset, reallyUseQuotedPrintable);
+    }
+
+    public void appendLineWithCharsetAndQPDetection(final String propertyName,
+            final List<String> parameterList, final List<String> rawValueList) {
+        boolean needCharset =
+            (mShouldAppendCharsetParam &&
+                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
+        boolean reallyUseQuotedPrintable =
+            (mShouldUseQuotedPrintable &&
+                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
+        appendLine(propertyName, parameterList, rawValueList,
+                needCharset, reallyUseQuotedPrintable);
+    }
+
+    /**
+     * Appends one line with a given property name and value.  
+     */
+    public void appendLine(final String propertyName, final String rawValue) {
+        appendLine(propertyName, rawValue, false, false);
+    }
+
+    public void appendLine(final String propertyName, final List<String> rawValueList) {
+        appendLine(propertyName, rawValueList, false, false);
+    }
+
+    public void appendLine(final String propertyName,
+            final String rawValue, final boolean needCharset,
+            boolean reallyUseQuotedPrintable) {
+        appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable);
+    }
+
+    public void appendLine(final String propertyName, final List<String> parameterList,
+            final String rawValue) {
+        appendLine(propertyName, parameterList, rawValue, false, false);
+    }
+
+    public void appendLine(final String propertyName, final List<String> parameterList,
+            final String rawValue, final boolean needCharset,
+            boolean reallyUseQuotedPrintable) {
+        mBuilder.append(propertyName);
+        if (parameterList != null && parameterList.size() > 0) {
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            appendTypeParameters(parameterList);
+        }
+        if (needCharset) {
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            mBuilder.append(mVCardCharsetParameter);
+        }
+
+        final String encodedValue;
+        if (reallyUseQuotedPrintable) {
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            mBuilder.append(VCARD_PARAM_ENCODING_QP);
+            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 that violation.
+            encodedValue = escapeCharacters(rawValue);
+        }
+
+        mBuilder.append(VCARD_DATA_SEPARATOR);
+        mBuilder.append(encodedValue);
+        mBuilder.append(VCARD_END_OF_LINE);
+    }
+
+    public void appendLine(final String propertyName, final List<String> rawValueList,
+            final boolean needCharset, boolean needQuotedPrintable) {
+        appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable);
+    }
+
+    public void appendLine(final String propertyName, final List<String> parameterList,
+            final List<String> rawValueList, final boolean needCharset,
+            final boolean needQuotedPrintable) {
+        mBuilder.append(propertyName);
+        if (parameterList != null && parameterList.size() > 0) {
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            appendTypeParameters(parameterList);
+        }
+        if (needCharset) {
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            mBuilder.append(mVCardCharsetParameter);
+        }
+        if (needQuotedPrintable) {
+            mBuilder.append(VCARD_PARAM_SEPARATOR);
+            mBuilder.append(VCARD_PARAM_ENCODING_QP);
+        }
+
+        mBuilder.append(VCARD_DATA_SEPARATOR);
+        boolean first = true;
+        for (String rawValue : rawValueList) {
+            final String encodedValue;
+            if (needQuotedPrintable) {
+                encodedValue = encodeQuotedPrintable(rawValue);
+            } else {
+                // TODO: one line may be too huge, which may be invalid in vCard 3.0
+                //        (which says "When generating a content line, lines longer than
+                //        75 characters SHOULD be folded"), though several
+                //        (even well-known) applications do not care this.
+                encodedValue = escapeCharacters(rawValue);
+            }
+
+            if (first) {
+                first = false;
+            } else {
+                mBuilder.append(VCARD_ITEM_SEPARATOR);
+            }
+            mBuilder.append(encodedValue);
+        }
+        mBuilder.append(VCARD_END_OF_LINE);
+    }
+
+    /**
+     * VCARD_PARAM_SEPARATOR must be appended before this method being called.
+     */
+    private void appendTypeParameters(final List<String> types) {
+        // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
+        // 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.isVersion30(mVCardType) || VCardConfig.isVersion40(mVCardType)) {
+                final String encoded = (VCardConfig.isVersion40(mVCardType) ?
+                        VCardUtils.toStringAsV40ParamValue(typeValue) :
+                        VCardUtils.toStringAsV30ParamValue(typeValue));
+                if (TextUtils.isEmpty(encoded)) {
+                    continue;
+                }
+
+                if (first) {
+                    first = false;
+                } else {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                }
+                appendTypeParameter(encoded);
+            } else {  // vCard 2.1
+                if (!VCardUtils.isV21Word(typeValue)) {
+                    continue;
+                }
+                if (first) {
+                    first = false;
+                } else {
+                    mBuilder.append(VCARD_PARAM_SEPARATOR);
+                }
+                appendTypeParameter(typeValue);
+            }
+        }
+    }
+
+    /**
+     * VCARD_PARAM_SEPARATOR must be appended before this method being called.
+     */
+    private void appendTypeParameter(final String type) {
+        appendTypeParameter(mBuilder, type);
+    }
+
+    private void appendTypeParameter(final StringBuilder builder, final String type) {
+        // Refrain from using appendType() so that "TYPE=" is not be appended when the
+        // device is DoCoMo's (just for safety).
+        //
+        // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
+        if (VCardConfig.isVersion40(mVCardType) ||
+                ((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) {
+            builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
+        }
+        builder.append(type);
+    }
+
+    /**
+     * Returns true when the property line should contain charset parameter
+     * information. This method may return true even when vCard version is 3.0.
+     *
+     * Strictly, adding charset information is invalid in VCard 3.0.
+     * However we'll add the info only when charset we use is not UTF-8
+     * in vCard 3.0 format, since parser side may be able to use the charset
+     * via this field, though we may encounter another problem by adding it.
+     *
+     * e.g. Japanese mobile phones use Shift_Jis while RFC 2426
+     * recommends UTF-8. By adding this field, parsers may be able
+     * to know this text is NOT UTF-8 but Shift_Jis.
+     */
+    private boolean shouldAppendCharsetParam(String...propertyValueList) {
+        if (!mShouldAppendCharsetParam) {
+            return false;
+        }
+        for (String propertyValue : propertyValueList) {
+            if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String encodeQuotedPrintable(final String str) {
+        if (TextUtils.isEmpty(str)) {
+            return "";
+        }
+
+        final StringBuilder builder = new StringBuilder();
+        int index = 0;
+        int lineCount = 0;
+        byte[] strArray = null;
+
+        try {
+            strArray = str.getBytes(mCharset);
+        } catch (UnsupportedEncodingException e) {
+            Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. "
+                    + "Try default charset");
+            strArray = str.getBytes();
+        }
+        while (index < strArray.length) {
+            builder.append(String.format("=%02X", strArray[index]));
+            index += 1;
+            lineCount += 3;
+
+            if (lineCount >= 67) {
+                // Specification requires CRLF must be inserted before the
+                // length of the line
+                // becomes more than 76.
+                // Assuming that the next character is a multi-byte character,
+                // it will become
+                // 6 bytes.
+                // 76 - 6 - 3 = 67
+                builder.append("=\r\n");
+                lineCount = 0;
+            }
+        }
+
+        return builder.toString();
+    }
+
+    /**
+     * Append '\' to the characters which should be escaped. The character set is different
+     * not only between vCard 2.1 and vCard 3.0 but also among each device.
+     *
+     * Note that Quoted-Printable string must not be input here.
+     */
+    @SuppressWarnings("fallthrough")
+    private String escapeCharacters(final String unescaped) {
+        if (TextUtils.isEmpty(unescaped)) {
+            return "";
+        }
+
+        final StringBuilder tmpBuilder = new StringBuilder();
+        final int length = unescaped.length();
+        for (int i = 0; i < length; i++) {
+            final char ch = unescaped.charAt(i);
+            switch (ch) {
+                case ';': {
+                    tmpBuilder.append('\\');
+                    tmpBuilder.append(';');
+                    break;
+                }
+                case '\r': {
+                    if (i + 1 < length) {
+                        char nextChar = unescaped.charAt(i);
+                        if (nextChar == '\n') {
+                            break;
+                        } else {
+                            // fall through
+                        }
+                    } else {
+                        // fall through
+                    }
+                }
+                case '\n': {
+                    // In vCard 2.1, there's no specification about this, while
+                    // vCard 3.0 explicitly requires this should be encoded to "\n".
+                    tmpBuilder.append("\\n");
+                    break;
+                }
+                case '\\': {
+                    if (mIsV30OrV40) {
+                        tmpBuilder.append("\\\\");
+                        break;
+                    } else {
+                        // fall through
+                    }
+                }
+                case '<':
+                case '>': {
+                    if (mIsDoCoMo) {
+                        tmpBuilder.append('\\');
+                        tmpBuilder.append(ch);
+                    } else {
+                        tmpBuilder.append(ch);
+                    }
+                    break;
+                }
+                case ',': {
+                    if (mIsV30OrV40) {
+                        tmpBuilder.append("\\,");
+                    } else {
+                        tmpBuilder.append(ch);
+                    }
+                    break;
+                }
+                default: {
+                    tmpBuilder.append(ch);
+                    break;
+                }
+            }
+        }
+        return tmpBuilder.toString();
+    }
+
+    @Override
+    public String toString() {
+        if (!mEndAppended) {
+            if (mIsDoCoMo) {
+                appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
+                appendLine(VCardConstants.PROPERTY_X_REDUCTION, "");
+                appendLine(VCardConstants.PROPERTY_X_NO, "");
+                appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, "");
+            }
+            appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD);
+            mEndAppended = true;
+        }
+        return mBuilder.toString();
+    }
+}
diff --git a/src/com/android/vcard/VCardConfig.java b/src/com/android/vcard/VCardConfig.java
new file mode 100644 (file)
index 0000000..c43d672
--- /dev/null
@@ -0,0 +1,511 @@
+/*
+ * 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 com.android.vcard;
+
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The class representing VCard related configurations. Useful static methods are not in this class
+ * but in VCardUtils.
+ */
+public class VCardConfig {
+    private static final String LOG_TAG = "VCardConfig";
+
+    /* package */ static final int LOG_LEVEL_NONE = 0;
+    /* package */ static final int LOG_LEVEL_PERFORMANCE_MEASUREMENT = 0x1;
+    /* package */ static final int LOG_LEVEL_SHOW_WARNING = 0x2;
+    /* package */ static final int LOG_LEVEL_VERBOSE =
+        LOG_LEVEL_PERFORMANCE_MEASUREMENT | LOG_LEVEL_SHOW_WARNING;
+
+    /* package */ static final int LOG_LEVEL = LOG_LEVEL_NONE;
+
+    /**
+     * <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";
+
+    /**
+     * The charset used when there's no information affbout what charset should be used to
+     * encode the binary given from vCard.
+     */
+    public static final String DEFAULT_IMPORT_CHARSET = "UTF-8";
+    public static final String DEFAULT_EXPORT_CHARSET = "UTF-8";
+
+    /**
+     * Do not use statically like "version == VERSION_V21"
+     */
+    public static final int VERSION_21 = 0;
+    public static final int VERSION_30 = 1;
+    public static final int VERSION_40 = 2;
+    public static final int VERSION_MASK = 3;
+
+    public static final int NAME_ORDER_DEFAULT = 0;
+    public static final int NAME_ORDER_EUROPE = 0x4;
+    public static final int NAME_ORDER_JAPANESE = 0x8;
+    private static final int NAME_ORDER_MASK = 0xC;
+
+    // 0x10 is reserved for safety
+
+    /**
+     * <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.
+     * </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.
+     * </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;
+
+    /**
+     * <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>
+     * 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>
+     * 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>
+     * 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>
+     * We are afraid of the case where some vCard importer also forget handling QP presuming QP is
+     * not used in such fields.
+     * </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>
+     * Again, we should not use this flag at all for complying vCard 2.1 spec.
+     * </p>
+     * <p>
+     * In vCard 3.0, Quoted-Printable is explicitly "prohibitted", so we don't need to care this
+     * kind of problem (hopefully).
+     * </p>
+     * @hide
+     */
+    public static final int FLAG_REFRAIN_QP_TO_NAME_PROPERTIES = 0x10000000;
+
+    /**
+     * <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>
+     * 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>
+     */
+    public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x08000000;
+
+    /**
+     * <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>
+     * Detail:
+     * How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0.
+     * </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:
+     * <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 indicating the vCard composer does touch nothing toward phone number Strings
+     * but leave it as is.
+     * </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
+     * just numbers, hypens, plus, etc. but not usual alphabets, while US mobile phones
+     * should get such characters. To make exported vCard simple for external parsers,
+     * we have used {@link PhoneNumberUtils#formatNumber(String)} during export, and
+     * removed unnecessary characters inside the number (e.g. "111-222-3333 (Miami)"
+     * 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>
+     * 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>
+     */
+    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;
+
+    /**
+     * <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 = 0x00800000;
+
+    //// The followings are VCard types available from importer/exporter. ////
+
+    /**
+     * <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>
+     * 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 =
+        (VERSION_21 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
+
+    /* package */ static String VCARD_TYPE_V21_GENERIC_STR = "v21_generic";
+    
+    /**
+     * <p>
+     * General vCard format with the version 3.0. Uses UTF-8 for the charset.
+     * </p>
+     * <p>
+     * Not fully ready yet. Use with caution when you use this.
+     * </p>
+     */
+    public static final int VCARD_TYPE_V30_GENERIC =
+        (VERSION_30 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
+
+    /* package */ static final String VCARD_TYPE_V30_GENERIC_STR = "v30_generic";
+
+    /**
+     * General vCard format with the version 4.0.
+     * @hide vCard 4.0 is not published yet.
+     */
+    public static final int VCARD_TYPE_V40_GENERIC =
+        (VERSION_40 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
+
+    /* package */ static final String VCARD_TYPE_V40_GENERIC_STR = "v40_generic";
+
+    /**
+     * <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>
+     */
+    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>
+     * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8.
+     * </p>
+     * <p>
+     * Not ready yet. Use with caution when you use this.
+     * </p>
+     */
+    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>
+     * The vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset.
+     * </p>
+     * <p>
+     * Not ready yet. Use with caution when you use this.
+     * </p>
+     */
+    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_STR = "v21_japanese_utf8";
+
+    /**
+     * <p>
+     * The vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset.
+     * </p>
+     * <p>
+     * Not ready yet. Use with caution when you use this.
+     * </p>
+     */
+    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_STR = "v30_japanese_utf8";
+
+    /**
+     * <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. 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 =
+        (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>
+     * The vCard format used in DoCoMo, which is one of Japanese mobile phone careers.
+     * </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>
+     * @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;
+
+    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_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);
+        sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE);
+        sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_MOBILE);
+        sJapaneseMobileTypeSet.add(VCARD_TYPE_DOCOMO);
+    }
+
+    public static int getVCardTypeFromString(final String vcardTypeString) {
+        final String loweredKey = vcardTypeString.toLowerCase();
+        if (sVCardTypeMap.containsKey(loweredKey)) {
+            return sVCardTypeMap.get(loweredKey);
+        } else if ("default".equalsIgnoreCase(vcardTypeString)) {
+            return VCARD_TYPE_DEFAULT;
+        } else {
+            Log.e(LOG_TAG, "Unknown vCard type String: \"" + vcardTypeString + "\"");
+            return VCARD_TYPE_DEFAULT;
+        }
+    }
+
+    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 !isVersion30(vcardType);
+    }
+
+    public static int getNameOrderType(final int vcardType) {
+        return vcardType & NAME_ORDER_MASK;
+    }
+
+    public static boolean usesAndroidSpecificProperty(final int vcardType) {
+        return ((vcardType & FLAG_USE_ANDROID_PROPERTY) != 0);
+    }
+
+    public static boolean usesDefactProperty(final int vcardType) {
+        return ((vcardType & FLAG_USE_DEFACT_PROPERTY) != 0);
+    }
+
+    public static boolean showPerformanceLog() {
+        return (VCardConfig.LOG_LEVEL & VCardConfig.LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0;
+    }
+
+    public static boolean shouldRefrainQPToNameProperties(final int vcardType) {
+       return (!shouldUseQuotedPrintable(vcardType) ||
+               ((vcardType & FLAG_REFRAIN_QP_TO_NAME_PROPERTIES) != 0));
+    }
+
+    public static boolean appendTypeParamName(final int vcardType) {
+        return (isVersion30(vcardType) || ((vcardType & FLAG_APPEND_TYPE_PARAM) != 0));
+    }
+
+    /**
+     * @return true if the device is Japanese and some Japanese convension is
+     * applied to creating "formatted" something like FORMATTED_ADDRESS.
+     */
+    public static boolean isJapaneseDevice(final int vcardType) {
+        // TODO: Some mask will be required so that this method wrongly interpret
+        //        Japanese"-like" vCard type.
+        //        e.g. VCARD_TYPE_V21_JAPANESE_SJIS | FLAG_APPEND_TYPE_PARAMS
+        return sJapaneseMobileTypeSet.contains(vcardType);
+    }
+
+    /* package */ static boolean refrainPhoneNumberFormatting(final int vcardType) {
+        return ((vcardType & FLAG_REFRAIN_PHONE_NUMBER_FORMATTING) != 0);
+    }
+
+    public static boolean needsToConvertPhoneticString(final int vcardType) {
+        return ((vcardType & FLAG_CONVERT_PHONETIC_NAME_STRINGS) != 0);
+    }
+
+    public static boolean onlyOneNoteFieldIsAvailable(final int vcardType) {
+        return vcardType == VCARD_TYPE_DOCOMO;
+    }
+
+    public static boolean isDoCoMo(final int vcardType) {
+        return ((vcardType & FLAG_DOCOMO) != 0);
+    }
+
+    private VCardConfig() {
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/vcard/VCardConstants.java b/src/com/android/vcard/VCardConstants.java
new file mode 100644 (file)
index 0000000..1c4e42b
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ * 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 com.android.vcard;
+
+/**
+ * Constants used in both exporter and importer code.
+ */
+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";
+    public static final String PROPERTY_VERSION = "VERSION";
+    public static final String PROPERTY_N = "N";
+    public static final String PROPERTY_FN = "FN";
+    public static final String PROPERTY_ADR = "ADR";
+    public static final String PROPERTY_EMAIL = "EMAIL";
+    public static final String PROPERTY_NOTE = "NOTE";
+    public static final String PROPERTY_ORG = "ORG";
+    public static final String PROPERTY_SOUND = "SOUND";  // Not fully supported.
+    public static final String PROPERTY_TEL = "TEL";
+    public static final String PROPERTY_TITLE = "TITLE";
+    public static final String PROPERTY_ROLE = "ROLE";
+    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 (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_IMPP = "IMPP";  // RFC 4770 (vCard 3.0) and vCard 4.0
+    public static final String PROPERTY_END = "END";
+
+    // defact SIP property which had been used till RFC 4770.
+    public static final String PROPERTY_X_SIP = "X-SIP";
+
+    // 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";  // (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.
+    
+    // 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 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";
+    public static final String PROPERTY_X_ICQ = "X-ICQ";
+    public static final String PROPERTY_X_JABBER = "X-JABBER";
+    public static final String PROPERTY_X_GOOGLE_TALK = "X-GOOGLE-TALK";
+    public static final String PROPERTY_X_SKYPE_USERNAME = "X-SKYPE-USERNAME";
+    // Properties only ContactsStruct has. We alse use this.
+    public static final String PROPERTY_X_QQ = "X-QQ";
+    public static final String PROPERTY_X_NETMEETING = "X-NETMEETING";
+
+    // Phone number for Skype, available as usual phone.
+    public static final String PROPERTY_X_SKYPE_PSTNNUMBER = "X-SKYPE-PSTNNUMBER";
+
+    // Property for Android-specific fields.
+    public static final String PROPERTY_X_ANDROID_CUSTOM = "X-ANDROID-CUSTOM";
+
+    // Properties for DoCoMo vCard.
+    public static final String PROPERTY_X_CLASS = "X-CLASS";
+    public static final String PROPERTY_X_REDUCTION = "X-REDUCTION";
+    public static final String PROPERTY_X_NO = "X-NO";
+    public static final String PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE";
+
+    public static final String PARAM_TYPE = "TYPE";
+
+    public static final String PARAM_TYPE_HOME = "HOME";
+    public static final String PARAM_TYPE_WORK = "WORK";
+    public static final String PARAM_TYPE_FAX = "FAX";
+    public static final String PARAM_TYPE_CELL = "CELL";
+    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.
+    //
+    // Note: Both vCard specs does not mention anything about the requirement for this parameter,
+    //       but there may be some vCard importer which will get confused with more than
+    //       one "PREF"s in one property name, while Android accepts them.
+    public static final String PARAM_TYPE_PREF = "PREF";
+
+    // Phone type parameters valid in vCard and known to ContactsContract, but not so common.
+    public static final String PARAM_TYPE_CAR = "CAR";
+    public static final String PARAM_TYPE_ISDN = "ISDN";
+    public static final String PARAM_TYPE_PAGER = "PAGER";
+    public static final String PARAM_TYPE_TLX = "TLX";  // Telex
+
+    // Phone types existing in vCard 2.1 but not known to ContactsContract.
+    public static final String PARAM_TYPE_MODEM = "MODEM";
+    public static final String PARAM_TYPE_MSG = "MSG";
+    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.
+    public static final String PARAM_PHONE_EXTRA_TYPE_CALLBACK = "CALLBACK";
+    public static final String PARAM_PHONE_EXTRA_TYPE_RADIO = "RADIO";
+    public static final String PARAM_PHONE_EXTRA_TYPE_TTY_TDD = "TTY-TDD";
+    public static final String PARAM_PHONE_EXTRA_TYPE_ASSISTANT = "ASSISTANT";
+    // vCard composer translates this type to "WORK" + "PREF". Just for parsing.
+    public static final String PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN = "COMPANY-MAIN";
+    // vCard composer translates this type to "VOICE" Just for parsing.
+    public static final String PARAM_PHONE_EXTRA_TYPE_OTHER = "OTHER";
+
+    // TYPE parameters for postal addresses.
+    public static final String PARAM_ADR_TYPE_PARCEL = "PARCEL";
+    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";
+
+    public interface ImportOnly {
+        public static final String PROPERTY_X_NICKNAME = "X-NICKNAME";
+        // Some device emits this "X-" parameter for expressing Google Talk,
+        // which is specifically invalid but should be always properly accepted, and emitted
+        // in some special case (for that device/application).
+        public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK";
+    }
+
+    //// 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;
+
+    private VCardConstants() {
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/vcard/VCardEntry.java b/src/com/android/vcard/VCardEntry.java
new file mode 100644 (file)
index 0000000..3422322
--- /dev/null
@@ -0,0 +1,1561 @@
+/*
+ * 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 com.android.vcard;
+
+import android.accounts.Account;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.OperationApplicationException;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+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;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+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.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class bridges between data structure of Contact app and VCard data.
+ */
+public class VCardEntry {
+    private static final String LOG_TAG = "VCardEntry";
+
+    private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK;
+
+    private static final Map<String, Integer> sImMap = new HashMap<String, Integer>();
+
+    static {
+        sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM);
+        sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN);
+        sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO);
+        sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ);
+        sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER);
+        sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE);
+        sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK);
+        sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE,
+                Im.PROTOCOL_GOOGLE_TALK);
+    }
+
+    public static class PhoneData {
+        public final int type;
+        public final String data;
+        public final String label;
+        // 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;
+            this.data = data;
+            this.label = label;
+            this.isPrimary = isPrimary;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof PhoneData)) {
+                return false;
+            }
+            PhoneData phoneData = (PhoneData)obj;
+            return (type == phoneData.type && data.equals(phoneData.data) &&
+                    label.equals(phoneData.label) && isPrimary == phoneData.isPrimary);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("type: %d, data: %s, label: %s, isPrimary: %s",
+                    type, data, label, isPrimary);
+        }
+    }
+
+    public static class EmailData {
+        public final int type;
+        public final String data;
+        // Used only when TYPE is TYPE_CUSTOM.
+        public final String label;
+        public boolean isPrimary;
+        public EmailData(int type, String data, String label, boolean isPrimary) {
+            this.type = type;
+            this.data = data;
+            this.label = label;
+            this.isPrimary = isPrimary;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof EmailData)) {
+                return false;
+            }
+            EmailData emailData = (EmailData)obj;
+            return (type == emailData.type && data.equals(emailData.data) &&
+                    label.equals(emailData.label) && isPrimary == emailData.isPrimary);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("type: %d, data: %s, label: %s, isPrimary: %s",
+                    type, data, label, isPrimary);
+        }
+    }
+
+    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;
+        public final String extendedAddress;
+        public final String street;
+        public final String localty;
+        public final String region;
+        public final String postalCode;
+        public final String country;
+        public final int type;
+        public final String label;
+        public boolean isPrimary;
+
+        public PostalData(final int type, final List<String> propValueList,
+                final String label, boolean isPrimary) {
+            this.type = type;
+            dataArray = new String[ADDR_MAX_DATA_SIZE];
+
+            int size = propValueList.size();
+            if (size > ADDR_MAX_DATA_SIZE) {
+                size = ADDR_MAX_DATA_SIZE;
+            }
+
+            // adr-value = 0*6(text-value ";") text-value
+            //           ; PO Box, Extended Address, Street, Locality, Region, Postal
+            //           ; Code, Country Name
+            //
+            // Use Iterator assuming List may be LinkedList, though actually it is
+            // always ArrayList in the current implementation.
+            int i = 0;
+            for (String addressElement : propValueList) {
+                dataArray[i] = addressElement;
+                if (++i >= size) {
+                    break;
+                }
+            }
+            while (i < ADDR_MAX_DATA_SIZE) {
+                dataArray[i++] = null;
+            }
+
+            this.pobox = dataArray[0];
+            this.extendedAddress = dataArray[1];
+            this.street = dataArray[2];
+            this.localty = dataArray[3];
+            this.region = dataArray[4];
+            this.postalCode = dataArray[5];
+            this.country = dataArray[6];
+            this.label = label;
+            this.isPrimary = isPrimary;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof PostalData)) {
+                return false;
+            }
+            final PostalData postalData = (PostalData)obj;
+            return (Arrays.equals(dataArray, postalData.dataArray) &&
+                    (type == postalData.type &&
+                            (type == StructuredPostal.TYPE_CUSTOM ?
+                                    (label == postalData.label) : true)) &&
+                    (isPrimary == postalData.isPrimary));
+        }
+
+        public String getFormattedAddress(final int vcardType) {
+            StringBuilder builder = new StringBuilder();
+            boolean empty = true;
+            if (VCardConfig.isJapaneseDevice(vcardType)) {
+                // In Japan, the order is reversed.
+                for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) {
+                    String addressPart = dataArray[i];
+                    if (!TextUtils.isEmpty(addressPart)) {
+                        if (!empty) {
+                            builder.append(' ');
+                        } else {
+                            empty = false;
+                        }
+                        builder.append(addressPart);
+                    }
+                }
+            } else {
+                for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) {
+                    String addressPart = dataArray[i];
+                    if (!TextUtils.isEmpty(addressPart)) {
+                        if (!empty) {
+                            builder.append(' ');
+                        } else {
+                            empty = false;
+                        }
+                        builder.append(addressPart);
+                    }
+                }
+            }
+
+            return builder.toString().trim();
+        }
+
+        @Override
+        public String toString() {
+            return String.format("type: %d, label: %s, isPrimary: %s",
+                    type, label, isPrimary);
+        }
+    }
+
+    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", 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,
+                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;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof OrganizationData)) {
+                return false;
+            }
+            OrganizationData organization = (OrganizationData)obj;
+            return (type == organization.type &&
+                    TextUtils.equals(companyName, organization.companyName) &&
+                    TextUtils.equals(departmentName, organization.departmentName) &&
+                    TextUtils.equals(titleName, organization.titleName) &&
+                    isPrimary == organization.isPrimary);
+        }
+
+        public String getFormattedString() {
+            final StringBuilder builder = new StringBuilder();
+            if (!TextUtils.isEmpty(companyName)) {
+                builder.append(companyName);
+            }
+
+            if (!TextUtils.isEmpty(departmentName)) {
+                if (builder.length() > 0) {
+                    builder.append(", ");
+                }
+                builder.append(departmentName);
+            }
+
+            if (!TextUtils.isEmpty(titleName)) {
+                if (builder.length() > 0) {
+                    builder.append(", ");
+                }
+                builder.append(titleName);
+            }
+
+            return builder.toString();
+        }
+
+        @Override
+        public String toString() {
+            return String.format(
+                    "type: %d, company: %s, department: %s, title: %s, isPrimary: %s",
+                    type, companyName, departmentName, titleName, isPrimary);
+        }
+    }
+
+    public static class ImData {
+        public final int protocol;
+        public final String customProtocol;
+        public final int type;
+        public final String data;
+        public final boolean isPrimary;
+
+        public ImData(final int protocol, final String customProtocol, final int type,
+                final String data, final boolean isPrimary) {
+            this.protocol = protocol;
+            this.customProtocol = customProtocol;
+            this.type = type;
+            this.data = data;
+            this.isPrimary = isPrimary;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof ImData)) {
+                return false;
+            }
+            ImData imData = (ImData)obj;
+            return (type == imData.type && protocol == imData.protocol
+                    && (customProtocol != null ? customProtocol.equals(imData.customProtocol) :
+                        (imData.customProtocol == null))
+                    && (data != null ? data.equals(imData.data) : (imData.data == null))
+                    && isPrimary == imData.isPrimary);
+        }
+
+        @Override
+        public String toString() {
+            return String.format(
+                    "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s",
+                    type, protocol, customProtocol, data, isPrimary);
+        }
+    }
+
+    public static class PhotoData {
+        public static final String FORMAT_FLASH = "SWF";
+        public final int type;
+        public final String formatName;  // used when type is not defined in ContactsContract.
+        public final byte[] photoBytes;
+        public final boolean isPrimary;
+
+        public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) {
+            this.type = type;
+            this.formatName = formatName;
+            this.photoBytes = photoBytes;
+            this.isPrimary = isPrimary;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof PhotoData)) {
+                return false;
+            }
+            PhotoData photoData = (PhotoData)obj;
+            return (type == photoData.type &&
+                    (formatName == null ? (photoData.formatName == null) :
+                            formatName.equals(photoData.formatName)) &&
+                    (Arrays.equals(photoBytes, photoData.photoBytes)) &&
+                    (isPrimary == photoData.isPrimary));
+        }
+
+        @Override
+        public String toString() {
+            return String.format("type: %d, format: %s: size: %d, isPrimary: %s",
+                    type, formatName, photoBytes.length, isPrimary);
+        }
+    }
+
+    /* package */ static class Property {
+        private String mPropertyName;
+        private Map<String, Collection<String>> mParameterMap =
+            new HashMap<String, Collection<String>>();
+        private List<String> mPropertyValueList = new ArrayList<String>();
+        private byte[] mPropertyBytes;
+
+        public void setPropertyName(final String propertyName) {
+            mPropertyName = propertyName;
+        }
+
+        public void addParameter(final String paramName, final String paramValue) {
+            Collection<String> values;
+            if (!mParameterMap.containsKey(paramName)) {
+                if (paramName.equals("TYPE")) {
+                    values = new HashSet<String>();
+                } else {
+                    values = new ArrayList<String>();
+                }
+                mParameterMap.put(paramName, values);
+            } else {
+                values = mParameterMap.get(paramName);
+            }
+            values.add(paramValue);
+        }
+
+        public void addToPropertyValueList(final String propertyValue) {
+            mPropertyValueList.add(propertyValue);
+        }
+
+        public void setPropertyBytes(final byte[] propertyBytes) {
+            mPropertyBytes = propertyBytes;
+        }
+
+        public final Collection<String> getParameters(String type) {
+            return mParameterMap.get(type);
+        }
+
+        public final List<String> getPropertyValueList() {
+            return mPropertyValueList;
+        }
+
+        public void clear() {
+            mPropertyName = null;
+            mParameterMap.clear();
+            mPropertyValueList.clear();
+            mPropertyBytes = null;
+        }
+    }
+
+    // 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;
+    private String mPrefix;
+    private String mSuffix;
+
+    // Used only when no family nor given name is found.
+    private String mFormattedName;
+
+    private String mPhoneticFamilyName;
+    private String mPhoneticGivenName;
+    private String mPhoneticMiddleName;
+
+    private String mPhoneticFullName;
+
+    private List<String> mNickNameList;
+
+    private String mDisplayName;
+
+    private String mBirthday;
+    private String mAnniversary;
+
+    private List<String> mNoteList;
+    private List<PhoneData> mPhoneList;
+    private List<EmailData> mEmailList;
+    private List<PostalData> mPostalList;
+    private List<OrganizationData> mOrganizationList;
+    private List<ImData> mImList;
+    private List<PhotoData> mPhotoList;
+    private List<String> mWebsiteList;
+    private Set<String> mSipSet;
+    private List<List<String>> mAndroidCustomPropertyList;
+
+    private final int mVCardType;
+    private final Account mAccount;
+
+    public VCardEntry() {
+        this(VCardConfig.VCARD_TYPE_V21_GENERIC);
+    }
+
+    public VCardEntry(int vcardType) {
+        this(vcardType, null);
+    }
+
+    public VCardEntry(int vcardType, Account account) {
+        mVCardType = vcardType;
+        mAccount = account;
+    }
+
+    private void addPhone(int type, String data, String label, boolean isPrimary) {
+        if (mPhoneList == null) {
+            mPhoneList = new ArrayList<PhoneData>();
+        }
+        final StringBuilder builder = new StringBuilder();
+        final String trimed = data.trim();
+        final String formattedNumber;
+        if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
+            formattedNumber = trimed;
+        } else {
+            final int length = trimed.length();
+            for (int i = 0; i < length; i++) {
+                char ch = trimed.charAt(i);
+                if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) {
+                    builder.append(ch);
+                }
+            }
+
+            final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType);
+            formattedNumber = PhoneNumberUtils.formatNumber(builder.toString()); //, formattingType);
+        }
+        PhoneData phoneData = new PhoneData(type, formattedNumber, label, isPrimary);
+        mPhoneList.add(phoneData);
+    }
+
+    private void addNickName(final String nickName) {
+        if (mNickNameList == null) {
+            mNickNameList = new ArrayList<String>();
+        }
+        mNickNameList.add(nickName);
+    }
+
+    private void addEmail(int type, String data, String label, boolean isPrimary){
+        if (mEmailList == null) {
+            mEmailList = new ArrayList<EmailData>();
+        }
+        mEmailList.add(new EmailData(type, data, label, isPrimary));
+    }
+
+    private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){
+        if (mPostalList == null) {
+            mPostalList = new ArrayList<PostalData>(0);
+        }
+        mPostalList.add(new PostalData(type, propValueList, label, isPrimary));
+    }
+
+    /**
+     * 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,
+            final String phoneticName,
+            final boolean isPrimary) {
+        if (mOrganizationList == null) {
+            mOrganizationList = new ArrayList<OrganizationData>();
+        }
+        mOrganizationList.add(new OrganizationData(type, companyName,
+                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
+     * does not have valid values (not including empty but only null). If there's no
+     * {@link OrganizationData} object, a new {@link OrganizationData} is created,
+     * whose title is set to null.
+     */
+    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;
+        }
+        final String companyName;
+        final String departmentName;
+        final int size = orgList.size();
+        switch (size) {
+            case 0: {
+                companyName = "";
+                departmentName = null;
+                break;
+            }
+            case 1: {
+                companyName = orgList.get(0);
+                departmentName = null;
+                break;
+            }
+            default: {  // More than 1.
+                companyName = orgList.get(0);
+                // We're not sure which is the correct string for department.
+                // In order to keep all the data, concatinate the rest of elements.
+                StringBuilder builder = new StringBuilder();
+                for (int i = 1; i < size; i++) {
+                    if (i > 1) {
+                        builder.append(' ');
+                    }
+                    builder.append(orgList.get(i));
+                }
+                departmentName = builder.toString();
+            }
+        }
+        if (mOrganizationList == null) {
+            // Create new first organization entry, with "null" title which may be
+            // added via handleTitleValue().
+            addNewOrganization(type, companyName, departmentName, null, phoneticName, isPrimary);
+            return;
+        }
+        for (OrganizationData organizationData : mOrganizationList) {
+            // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty.
+            // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null.
+            if (organizationData.companyName == null &&
+                    organizationData.departmentName == null) {
+                // Probably the "TITLE" property comes before the "ORG" property via
+                // handleTitleLine().
+                organizationData.companyName = companyName;
+                organizationData.departmentName = departmentName;
+                organizationData.isPrimary = isPrimary;
+                return;
+            }
+        }
+        // No OrganizatioData is available. Create another one, with "null" title, which may be
+        // added via handleTitleValue().
+        addNewOrganization(type, companyName, departmentName, null, phoneticName, isPrimary);
+    }
+
+    /**
+     * Set "title" value to the appropriate data. If there's more than one
+     * OrganizationData objects, this input is attached to the last one which does not
+     * have valid title value (not including empty but only null). If there's no
+     * OrganizationData object, a new OrganizationData is created, whose company name is
+     * set to null.
+     */
+    private void handleTitleValue(final String title) {
+        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, null, false);
+            return;
+        }
+        for (OrganizationData organizationData : mOrganizationList) {
+            if (organizationData.titleName == null) {
+                organizationData.titleName = title;
+                return;
+            }
+        }
+        // No Organization is available. Create another one, with "null" other info, which may be
+        // added via handleOrgValue().
+        addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, null, false);
+    }
+
+    private void addIm(int protocol, String customProtocol, int type,
+            String propValue, boolean isPrimary) {
+        if (mImList == null) {
+            mImList = new ArrayList<ImData>();
+        }
+        mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary));
+    }
+
+    private void addNote(final String note) {
+        if (mNoteList == null) {
+            mNoteList = new ArrayList<String>(1);
+        }
+        mNoteList.add(note);
+    }
+
+    private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) {
+        if (mPhotoList == null) {
+            mPhotoList = new ArrayList<PhotoData>(1);
+        }
+        final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary);
+        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(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 (paramValues == null || (size = paramValues.size()) < 1) {
+            return;
+        }
+        if (size > 5) {
+            size = 5;
+        }
+
+        switch (size) {
+        // 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);
+        }
+    }
+
+    /**
+     * Note: Some Japanese mobile phones use this field for phonetic name,
+     *       since vCard 2.1 does not have "SORT-STRING" type.
+     *       Also, in some cases, the field has some ';'s in it.
+     *       Assume the ';' means the same meaning in N property
+     */
+    @SuppressWarnings("fallthrough")
+    private void handlePhoneticNameFromSound(List<String> elems) {
+        if (!(TextUtils.isEmpty(mPhoneticFamilyName) &&
+                TextUtils.isEmpty(mPhoneticMiddleName) &&
+                TextUtils.isEmpty(mPhoneticGivenName))) {
+            // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found.
+            // Ignore "SOUND;X-IRMC-N".
+            return;
+        }
+
+        int size;
+        if (elems == null || (size = elems.size()) < 1) {
+            return;
+        }
+
+        // Assume that the order is "Family, Given, Middle".
+        // This is not from specification but mere assumption. Some Japanese phones use this order.
+        if (size > 3) {
+            size = 3;
+        }
+
+        if (elems.get(0).length() > 0) {
+            boolean onlyFirstElemIsNonEmpty = true;
+            for (int i = 1; i < size; i++) {
+                if (elems.get(i).length() > 0) {
+                    onlyFirstElemIsNonEmpty = false;
+                    break;
+                }
+            }
+            if (onlyFirstElemIsNonEmpty) {
+                final String[] namesArray = elems.get(0).split(" ");
+                final int nameArrayLength = namesArray.length;
+                if (nameArrayLength == 3) {
+                    // Assume the string is "Family Middle Given".
+                    mPhoneticFamilyName = namesArray[0];
+                    mPhoneticMiddleName = namesArray[1];
+                    mPhoneticGivenName = namesArray[2];
+                } else if (nameArrayLength == 2) {
+                    // Assume the string is "Family Given" based on the Japanese mobile
+                    // phones' preference.
+                    mPhoneticFamilyName = namesArray[0];
+                    mPhoneticGivenName = namesArray[1];
+                } else {
+                    mPhoneticFullName = elems.get(0);
+                }
+                return;
+            }
+        }
+
+        switch (size) {
+            // fallthrough
+            case 3: mPhoneticMiddleName = elems.get(2);
+            case 2: mPhoneticGivenName = elems.get(1);
+            default: mPhoneticFamilyName = elems.get(0);
+        }
+    }
+
+    public void addProperty(final Property property) {
+        final String propName = property.mPropertyName;
+        final Map<String, Collection<String>> paramMap = property.mParameterMap;
+        final List<String> propValueList = property.mPropertyValueList;
+        byte[] propBytes = property.mPropertyBytes;
+
+        if (propValueList.size() == 0) {
+            return;
+        }
+        final String propValue = listToString(propValueList).trim();
+
+        if (propName.equals(VCardConstants.PROPERTY_VERSION)) {
+            // vCard version. Ignore this.
+        } else if (propName.equals(VCardConstants.PROPERTY_FN)) {
+            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.
+            mFormattedName = propValue;
+        } else if (propName.equals(VCardConstants.PROPERTY_N)) {
+            handleNProperty(propValueList, paramMap);
+        } else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) {
+            mPhoneticFullName = propValue;
+        } else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) ||
+                propName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) {
+            addNickName(propValue);
+        } else if (propName.equals(VCardConstants.PROPERTY_SOUND)) {
+            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
+            if (typeCollection != null
+                    && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) {
+                // As of 2009-10-08, Parser side does not split a property value into separated
+                // values using ';' (in other words, propValueList.size() == 1),
+                // 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, mVCardType);
+                handlePhoneticNameFromSound(phoneticNameList);
+            } else {
+                // Ignore this field since Android cannot understand what it is.
+            }
+        } else if (propName.equals(VCardConstants.PROPERTY_ADR)) {
+            boolean valuesAreAllEmpty = true;
+            for (String value : propValueList) {
+                if (value.length() > 0) {
+                    valuesAreAllEmpty = false;
+                    break;
+                }
+            }
+            if (valuesAreAllEmpty) {
+                return;
+            }
+
+            int type = -1;
+            String label = "";
+            boolean isPrimary = false;
+            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
+            if (typeCollection != null) {
+                for (String typeString : typeCollection) {
+                    typeString = typeString.toUpperCase();
+                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
+                        isPrimary = true;
+                    } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) {
+                        type = StructuredPostal.TYPE_HOME;
+                        label = "";
+                    } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK) ||
+                            typeString.equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) {
+                        // "COMPANY" seems emitted by Windows Mobile, which is not
+                        // specifically supported by vCard 2.1. We assume this is same
+                        // as "WORK".
+                        type = StructuredPostal.TYPE_WORK;
+                        label = "";
+                    } else if (typeString.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) ||
+                            typeString.equals(VCardConstants.PARAM_ADR_TYPE_DOM) ||
+                            typeString.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) {
+                        // We do not have any appropriate way to store this information.
+                    } else {
+                        if (typeString.startsWith("X-") && type < 0) {
+                            typeString = typeString.substring(2);
+                        }
+                        // vCard 3.0 allows iana-token. Also some vCard 2.1 exporters
+                        // emit non-standard types. We do not handle their values now.
+                        type = StructuredPostal.TYPE_CUSTOM;
+                        label = typeString;
+                    }
+                }
+            }
+            // We use "HOME" as default
+            if (type < 0) {
+                type = StructuredPostal.TYPE_HOME;
+            }
+
+            addPostal(type, propValueList, label, isPrimary);
+        } else if (propName.equals(VCardConstants.PROPERTY_EMAIL)) {
+            int type = -1;
+            String label = null;
+            boolean isPrimary = false;
+            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
+            if (typeCollection != null) {
+                for (String typeString : typeCollection) {
+                    typeString = typeString.toUpperCase();
+                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
+                        isPrimary = true;
+                    } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) {
+                        type = Email.TYPE_HOME;
+                    } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK)) {
+                        type = Email.TYPE_WORK;
+                    } else if (typeString.equals(VCardConstants.PARAM_TYPE_CELL)) {
+                        type = Email.TYPE_MOBILE;
+                    } else {
+                        if (typeString.startsWith("X-") && type < 0) {
+                            typeString = typeString.substring(2);
+                        }
+                        // vCard 3.0 allows iana-token.
+                        // We may have INTERNET (specified in vCard spec),
+                        // SCHOOL, etc.
+                        type = Email.TYPE_CUSTOM;
+                        label = typeString;
+                    }
+                }
+            }
+            if (type < 0) {
+                type = Email.TYPE_OTHER;
+            }
+            addEmail(type, propValue, label, isPrimary);
+        } else if (propName.equals(VCardConstants.PROPERTY_ORG)) {
+            // vCard specification does not specify other types.
+            final int type = Organization.TYPE_WORK;
+            boolean isPrimary = false;
+            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
+            if (typeCollection != null) {
+                for (String typeString : typeCollection) {
+                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
+                        isPrimary = true;
+                    }
+                }
+            }
+            handleOrgValue(type, propValueList, paramMap, isPrimary);
+        } else if (propName.equals(VCardConstants.PROPERTY_TITLE)) {
+            handleTitleValue(propValue);
+        } else if (propName.equals(VCardConstants.PROPERTY_ROLE)) {
+            // This conflicts with TITLE. Ignore for now...
+            // handleTitleValue(propValue);
+        } else if (propName.equals(VCardConstants.PROPERTY_PHOTO) ||
+                propName.equals(VCardConstants.PROPERTY_LOGO)) {
+            Collection<String> paramMapValue = paramMap.get("VALUE");
+            if (paramMapValue != null && paramMapValue.contains("URL")) {
+                // Currently we do not have appropriate example for testing this case.
+            } else {
+                final Collection<String> typeCollection = paramMap.get("TYPE");
+                String formatName = null;
+                boolean isPrimary = false;
+                if (typeCollection != null) {
+                    for (String typeValue : typeCollection) {
+                        if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) {
+                            isPrimary = true;
+                        } else if (formatName == null){
+                            formatName = typeValue;
+                        }
+                    }
+                }
+                addPhotoBytes(formatName, propBytes, isPrimary);
+            }
+        } else if (propName.equals(VCardConstants.PROPERTY_TEL)) {
+            final String phoneNumber;
+            if (VCardConfig.isVersion40(mVCardType)) {
+                // Given propValue is in URI format, not in phone number format used until
+                // vCard 3.0.
+                if (propValue.startsWith("sip:") ) {
+                    if (propValue.length() > 4) {
+                        if (mSipSet == null) {
+                            mSipSet = new LinkedHashSet<String>();
+                        }
+                        mSipSet.add(propValue.substring(4));
+                    }
+                    return;
+                } else if (propValue.startsWith("tel:")) {
+                    phoneNumber = propValue.substring(4);
+                } else {
+                    // We don't know appropriate way to handle the other schemas. Also,
+                    // we may still have non-URI phone number. To keep given data as much as
+                    // we can, just save original value here.
+                    phoneNumber = propValue;
+                }
+            } else {
+                phoneNumber = propValue;
+            }
+
+            if (propValue.length() == 0) {
+                return;
+            }
+            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
+            final Object typeObject =
+                    VCardUtils.getPhoneTypeFromStrings(typeCollection, phoneNumber);
+            final int type;
+            final String label;
+            if (typeObject instanceof Integer) {
+                type = (Integer)typeObject;
+                label = null;
+            } else {
+                type = Phone.TYPE_CUSTOM;
+                label = typeObject.toString();
+            }
+
+            final boolean isPrimary;
+            if (typeCollection != null &&
+                    typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
+                isPrimary = true;
+            } else {
+                isPrimary = false;
+            }
+            addPhone(type, phoneNumber, label, isPrimary);
+        } else if (propName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) {
+            // The phone number available via Skype.
+            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
+            final int type = Phone.TYPE_OTHER;
+            final boolean isPrimary;
+            if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
+                isPrimary = true;
+            } else {
+                isPrimary = false;
+            }
+            addPhone(type, propValue, null, isPrimary);
+        } else if (sImMap.containsKey(propName)) {
+            final int protocol = sImMap.get(propName);
+            boolean isPrimary = false;
+            int type = -1;
+            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
+            if (typeCollection != null) {
+                for (String typeString : typeCollection) {
+                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
+                        isPrimary = true;
+                    } else if (type < 0) {
+                        if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) {
+                            type = Im.TYPE_HOME;
+                        } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) {
+                            type = Im.TYPE_WORK;
+                        }
+                    }
+                }
+            }
+            if (type < 0) {
+                type = Im.TYPE_HOME;
+            }
+            addIm(protocol, null, type, propValue, isPrimary);
+        } else if (propName.equals(VCardConstants.PROPERTY_NOTE)) {
+            addNote(propValue);
+        } else if (propName.equals(VCardConstants.PROPERTY_URL)) {
+            if (mWebsiteList == null) {
+                mWebsiteList = new ArrayList<String>(1);
+            }
+            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)) {
+            mPhoneticMiddleName = propValue;
+        } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) {
+            mPhoneticFamilyName = propValue;
+        } else if (propName.equals(VCardConstants.PROPERTY_IMPP)) {
+            // See also RFC 4770 (for vCard 3.0)
+            if (propValue.startsWith("sip:") && propValue.length() > 4) {
+                if (mSipSet == null) {
+                    mSipSet = new LinkedHashSet<String>();
+                }
+                mSipSet.add(propValue.substring(4));
+            }
+        } else if (propName.equals(VCardConstants.PROPERTY_X_SIP)) {
+            if (!TextUtils.isEmpty(propValue)) {
+                if (mSipSet == null) {
+                    mSipSet = new LinkedHashSet<String>();
+                }
+
+                if (propValue.startsWith("sip:")) {
+                    if (propValue.length() > 4) {
+                        mSipSet.add(propValue.substring(4));
+                    } else {
+                        // Empty sip value. Ignore.
+                    }
+                } else {
+                    mSipSet.add(propValue);
+                }
+            }
+        } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
+            final List<String> customPropertyList =
+                VCardUtils.constructListFromValue(propValue, mVCardType);
+            handleAndroidCustomProperty(customPropertyList);
+        } else {
+        }
+        // Be careful when adding some logic here, as some blocks above may use "return".
+    }
+
+    private void handleAndroidCustomProperty(final List<String> customPropertyList) {
+        if (mAndroidCustomPropertyList == null) {
+            mAndroidCustomPropertyList = new ArrayList<List<String>>();
+        }
+        mAndroidCustomPropertyList.add(customPropertyList);
+    }
+
+    /**
+     * Construct the display name. The constructed data must not be null.
+     */
+    private void constructDisplayName() {
+        // FullName (created via "FN" or "NAME" field) is prefered.
+        if (!TextUtils.isEmpty(mFormattedName)) {
+            mDisplayName = mFormattedName;
+        } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) {
+            mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
+                    mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix);
+        } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) &&
+                TextUtils.isEmpty(mPhoneticGivenName))) {
+            mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
+                    mPhoneticFamilyName, mPhoneticMiddleName, mPhoneticGivenName);
+        } else if (mEmailList != null && mEmailList.size() > 0) {
+            mDisplayName = mEmailList.get(0).data;
+        } else if (mPhoneList != null && mPhoneList.size() > 0) {
+            mDisplayName = mPhoneList.get(0).data;
+        } else if (mPostalList != null && mPostalList.size() > 0) {
+            mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType);
+        } else if (mOrganizationList != null && mOrganizationList.size() > 0) {
+            mDisplayName = mOrganizationList.get(0).getFormattedString();
+        }
+
+        if (mDisplayName == null) {
+            mDisplayName = "";
+        }
+    }
+
+    /**
+     * Consolidate several fielsds (like mName) using name candidates,
+     */
+    public void consolidateFields() {
+        constructDisplayName();
+
+        if (mPhoneticFullName != null) {
+            mPhoneticFullName = mPhoneticFullName.trim();
+        }
+    }
+
+    public Uri pushIntoContentResolver(ContentResolver resolver) {
+        ArrayList<ContentProviderOperation> operationList =
+            new ArrayList<ContentProviderOperation>();
+        // After applying the batch the first result's Uri is returned so it is important that
+        // the RawContact is the first operation that gets inserted into the list
+        ContentProviderOperation.Builder builder =
+            ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
+        String myGroupsId = null;
+        if (mAccount != null) {
+            builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
+            builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
+        } else {
+            builder.withValue(RawContacts.ACCOUNT_NAME, null);
+            builder.withValue(RawContacts.ACCOUNT_TYPE, null);
+        }
+        operationList.add(builder.build());
+
+        if (!nameFieldsAreEmpty()) {
+            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+            builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0);
+            builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+
+            builder.withValue(StructuredName.GIVEN_NAME, mGivenName);
+            builder.withValue(StructuredName.FAMILY_NAME, mFamilyName);
+            builder.withValue(StructuredName.MIDDLE_NAME, mMiddleName);
+            builder.withValue(StructuredName.PREFIX, mPrefix);
+            builder.withValue(StructuredName.SUFFIX, mSuffix);
+
+            if (!(TextUtils.isEmpty(mPhoneticGivenName)
+                    && TextUtils.isEmpty(mPhoneticFamilyName)
+                    && TextUtils.isEmpty(mPhoneticMiddleName))) {
+                builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName);
+                builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName);
+                builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName);
+            } else if (!TextUtils.isEmpty(mPhoneticFullName)) {
+                builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticFullName);
+            }
+
+            builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName());
+            operationList.add(builder.build());
+        }
+
+        if (mNickNameList != null && mNickNameList.size() > 0) {
+            for (String nickName : mNickNameList) {
+                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0);
+                builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
+                builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+                builder.withValue(Nickname.NAME, nickName);
+                operationList.add(builder.build());
+            }
+        }
+
+        if (mPhoneList != null) {
+            for (PhoneData phoneData : mPhoneList) {
+                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0);
+                builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+
+                builder.withValue(Phone.TYPE, phoneData.type);
+                if (phoneData.type == Phone.TYPE_CUSTOM) {
+                    builder.withValue(Phone.LABEL, phoneData.label);
+                }
+                builder.withValue(Phone.NUMBER, phoneData.data);
+                if (phoneData.isPrimary) {
+                    builder.withValue(Phone.IS_PRIMARY, 1);
+                }
+                operationList.add(builder.build());
+            }
+        }
+
+        if (mOrganizationList != null) {
+            for (OrganizationData organizationData : mOrganizationList) {
+                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0);
+                builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+                builder.withValue(Organization.TYPE, organizationData.type);
+                if (organizationData.companyName != null) {
+                    builder.withValue(Organization.COMPANY, organizationData.companyName);
+                }
+                if (organizationData.departmentName != null) {
+                    builder.withValue(Organization.DEPARTMENT, organizationData.departmentName);
+                }
+                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);
+                }
+                operationList.add(builder.build());
+            }
+        }
+
+        if (mEmailList != null) {
+            for (EmailData emailData : mEmailList) {
+                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                builder.withValueBackReference(Email.RAW_CONTACT_ID, 0);
+                builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+
+                builder.withValue(Email.TYPE, emailData.type);
+                if (emailData.type == Email.TYPE_CUSTOM) {
+                    builder.withValue(Email.LABEL, emailData.label);
+                }
+                builder.withValue(Email.DATA, emailData.data);
+                if (emailData.isPrimary) {
+                    builder.withValue(Data.IS_PRIMARY, 1);
+                }
+                operationList.add(builder.build());
+            }
+        }
+
+        if (mPostalList != null) {
+            for (PostalData postalData : mPostalList) {
+                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                builder.withValueBackReference(Event.RAW_CONTACT_ID, 0);
+                VCardUtils.insertStructuredPostalDataUsingContactsStruct(
+                        mVCardType, builder, postalData);
+                operationList.add(builder.build());
+            }
+        }
+
+        if (mImList != null) {
+            for (ImData imData : mImList) {
+                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                builder.withValueBackReference(Im.RAW_CONTACT_ID, 0);
+                builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+                builder.withValue(Im.TYPE, imData.type);
+                builder.withValue(Im.PROTOCOL, imData.protocol);
+                builder.withValue(Im.DATA, imData.data);
+                if (imData.protocol == Im.PROTOCOL_CUSTOM) {
+                    builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol);
+                }
+                if (imData.isPrimary) {
+                    builder.withValue(Data.IS_PRIMARY, 1);
+                }
+                operationList.add(builder.build());
+            }
+        }
+
+        if (mNoteList != null) {
+            for (String note : mNoteList) {
+                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                builder.withValueBackReference(Note.RAW_CONTACT_ID, 0);
+                builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
+                builder.withValue(Note.NOTE, note);
+                operationList.add(builder.build());
+            }
+        }
+
+        if (mPhotoList != null) {
+            for (PhotoData photoData : mPhotoList) {
+                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0);
+                builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+                builder.withValue(Photo.PHOTO, photoData.photoBytes);
+                if (photoData.isPrimary) {
+                    builder.withValue(Photo.IS_PRIMARY, 1);
+                }
+                operationList.add(builder.build());
+            }
+        }
+
+        if (mWebsiteList != null) {
+            for (String website : mWebsiteList) {
+                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                builder.withValueBackReference(Website.RAW_CONTACT_ID, 0);
+                builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
+                builder.withValue(Website.URL, website);
+                // There's no information about the type of URL in vCard.
+                // We use TYPE_HOMEPAGE for safety.
+                builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
+                operationList.add(builder.build());
+            }
+        }
+
+        if (!TextUtils.isEmpty(mBirthday)) {
+            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, mBirthday);
+            builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
+            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 (mSipSet != null && !mSipSet.isEmpty()) {
+            for (String sipAddress : mSipSet) {
+                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                builder.withValueBackReference(Event.RAW_CONTACT_ID, 0);
+                builder.withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+                builder.withValue(SipAddress.SIP_ADDRESS, sipAddress);
+                operationList.add(builder.build());
+            }
+        }
+
+        if (mAndroidCustomPropertyList != null) {
+            for (List<String> customPropertyList : mAndroidCustomPropertyList) {
+                int size = customPropertyList.size();
+                if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) {
+                    continue;
+                } else if (size > VCardConstants.MAX_DATA_COLUMN + 1) {
+                    size = VCardConstants.MAX_DATA_COLUMN + 1;
+                    customPropertyList =
+                        customPropertyList.subList(0, VCardConstants.MAX_DATA_COLUMN + 2);
+                }
+
+                int i = 0;
+                for (final String customPropertyValue : customPropertyList) {
+                    if (i == 0) {
+                        final String mimeType = customPropertyValue;
+                        builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+                        builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
+                        builder.withValue(Data.MIMETYPE, mimeType);
+                    } else {  // 1 <= i && i <= MAX_DATA_COLUMNS
+                        if (!TextUtils.isEmpty(customPropertyValue)) {
+                            builder.withValue("data" + i, customPropertyValue);
+                        }
+                    }
+
+                    i++;
+                }
+                operationList.add(builder.build());
+            }
+        }
+
+        if (myGroupsId != null) {
+            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+            builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
+            builder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
+            builder.withValue(GroupMembership.GROUP_SOURCE_ID, myGroupsId);
+            operationList.add(builder.build());
+        }
+
+        try {
+            ContentProviderResult[] results = resolver.applyBatch(
+                        ContactsContract.AUTHORITY, operationList);
+            // the first result is always the raw_contact. return it's uri so
+            // that it can be found later. do null checking for badly behaving
+            // ContentResolvers
+            return (results == null || results.length == 0 || results[0] == null)
+                ? null
+                : results[0].uri;
+        } catch (RemoteException e) {
+            Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
+            return null;
+        } catch (OperationApplicationException e) {
+            Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
+            return null;
+        }
+    }
+
+    public static VCardEntry buildFromResolver(ContentResolver resolver) {
+        return buildFromResolver(resolver, Contacts.CONTENT_URI);
+    }
+
+    public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) {
+
+        return null;
+    }
+
+    private boolean nameFieldsAreEmpty() {
+        return (TextUtils.isEmpty(mFamilyName)
+                && TextUtils.isEmpty(mMiddleName)
+                && TextUtils.isEmpty(mGivenName)
+                && TextUtils.isEmpty(mPrefix)
+                && TextUtils.isEmpty(mSuffix)
+                && TextUtils.isEmpty(mFormattedName)
+                && TextUtils.isEmpty(mPhoneticFamilyName)
+                && TextUtils.isEmpty(mPhoneticMiddleName)
+                && TextUtils.isEmpty(mPhoneticGivenName)
+                && TextUtils.isEmpty(mPhoneticFullName));
+    }
+
+    public boolean isIgnorable() {
+        return getDisplayName().length() == 0;
+    }
+
+    private String listToString(List<String> list){
+        final int size = list.size();
+        if (size > 1) {
+            StringBuilder builder = new StringBuilder();
+            int i = 0;
+            for (String type : list) {
+                builder.append(type);
+                if (i < size - 1) {
+                    builder.append(";");
+                }
+            }
+            return builder.toString();
+        } else if (size == 1) {
+            return list.get(0);
+        } else {
+            return "";
+        }
+    }
+
+    // All getter methods should be used carefully, since they may change
+    // in the future as of 2009-10-05, on which I cannot be sure this structure
+    // is completely consolidated.
+    //
+    // Also note that these getter methods should be used only after
+    // all properties being pushed into this object. If not, incorrect
+    // value will "be stored in the local cache and" be returned to you.
+
+    public String getFamilyName() {
+        return mFamilyName;
+    }
+
+    public String getGivenName() {
+        return mGivenName;
+    }
+
+    public String getMiddleName() {
+        return mMiddleName;
+    }
+
+    public String getPrefix() {
+        return mPrefix;
+    }
+
+    public String getSuffix() {
+        return mSuffix;
+    }
+
+    public String getFullName() {
+        return mFormattedName;
+    }
+
+    public String getPhoneticFamilyName() {
+        return mPhoneticFamilyName;
+    }
+
+    public String getPhoneticGivenName() {
+        return mPhoneticGivenName;
+    }
+
+    public String getPhoneticMiddleName() {
+        return mPhoneticMiddleName;
+    }
+
+    public String getPhoneticFullName() {
+        return mPhoneticFullName;
+    }
+
+    public final List<String> getNickNameList() {
+        return mNickNameList;
+    }
+
+    public String getBirthday() {
+        return mBirthday;
+    }
+
+    public final List<String> getNotes() {
+        return mNoteList;
+    }
+
+    public final List<PhoneData> getPhoneList() {
+        return mPhoneList;
+    }
+
+    public final List<EmailData> getEmailList() {
+        return mEmailList;
+    }
+
+    public final List<PostalData> getPostalList() {
+        return mPostalList;
+    }
+
+    public final List<OrganizationData> getOrganizationList() {
+        return mOrganizationList;
+    }
+
+    public final List<ImData> getImList() {
+        return mImList;
+    }
+
+    public final List<PhotoData> getPhotoList() {
+        return mPhotoList;
+    }
+
+    public final List<String> getWebsiteList() {
+        return mWebsiteList;
+    }
+
+    public String getDisplayName() {
+        if (mDisplayName == null) {
+            constructDisplayName();
+        }
+        return mDisplayName;
+    }
+}
diff --git a/src/com/android/vcard/VCardEntryCommitter.java b/src/com/android/vcard/VCardEntryCommitter.java
new file mode 100644 (file)
index 0000000..7bd314e
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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 com.android.vcard;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * <P>
+ * {@link VCardEntryHandler} implementation which commits the entry to ContentResolver.
+ * </P>
+ * <P>
+ * Note:<BR />
+ * Each vCard may contain big photo images encoded by BASE64,
+ * If we store all vCard entries in memory, OutOfMemoryError may be thrown.
+ * Thus, this class push each VCard entry into ContentResolver immediately.
+ * </P>
+ */
+public class VCardEntryCommitter implements VCardEntryHandler {
+    public static String LOG_TAG = "VCardEntryComitter";
+
+    private final ContentResolver mContentResolver;
+    private long mTimeToCommit;
+    private ArrayList<Uri> mCreatedUris = new ArrayList<Uri>();
+
+    public VCardEntryCommitter(ContentResolver resolver) {
+        mContentResolver = resolver;
+    }
+
+    public void onStart() {
+    }
+
+    public void onEnd() {
+        if (VCardConfig.showPerformanceLog()) {
+            Log.d(LOG_TAG, String.format("time to commit entries: %d ms", mTimeToCommit));
+        }
+    }
+
+    public void onEntryCreated(final VCardEntry vcardEntry) {
+        long start = System.currentTimeMillis();
+        mCreatedUris.add(vcardEntry.pushIntoContentResolver(mContentResolver));
+        mTimeToCommit += System.currentTimeMillis() - start;
+    }
+
+    /**
+     * Returns the list of created Uris. This list should not be modified by the caller as it is
+     * not a clone.
+     */
+   public ArrayList<Uri> getCreatedUris() {
+        return mCreatedUris;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/vcard/VCardEntryConstructor.java b/src/com/android/vcard/VCardEntryConstructor.java
new file mode 100644 (file)
index 0000000..8acad7e
--- /dev/null
@@ -0,0 +1,234 @@
+/*
+ * 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 com.android.vcard;
+
+import android.accounts.Account;
+import android.text.TextUtils;
+import android.util.Base64;
+//import android.util.CharsetUtils;
+import android.util.Log;
+
+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";
+
+    private VCardEntry.Property mCurrentProperty = new VCardEntry.Property();
+    private VCardEntry mCurrentVCardEntry;
+    private String mParamType;
+    
+    // The charset using which {@link VCardInterpreter} parses the text.
+    // Each String is first decoded into binary stream with this charset, and encoded back
+    // to "target charset", which may be explicitly specified by the vCard with "CHARSET"
+    // property or implicitly mentioned by its version (e.g. vCard 3.0 recommends UTF-8).
+    private final String mSourceCharset;
+
+    private final boolean mStrictLineBreaking;
+    private final int mVCardType;
+    private final Account mAccount;
+    
+    // For measuring performance.
+    private long mTimePushIntoContentResolver;
+
+    private final List<VCardEntryHandler> mEntryHandlers = new ArrayList<VCardEntryHandler>();
+
+    public VCardEntryConstructor() {
+        this(VCardConfig.VCARD_TYPE_V21_GENERIC, null);
+    }
+
+    public VCardEntryConstructor(final int vcardType) {
+        this(vcardType, null, null, false);
+    }
+
+    public VCardEntryConstructor(final int vcardType, final Account account) {
+        this(vcardType, account, null, false);
+    }
+
+    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) {
+            mSourceCharset = inputCharset;
+        } else {
+            mSourceCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET;
+        }
+        mStrictLineBreaking = strictLineBreakParsing;
+        mVCardType = vcardType;
+        mAccount = account;
+    }
+
+    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();
+        }
+    }
+
+    public void clear() {
+        mCurrentVCardEntry = null;
+        mCurrentProperty = new VCardEntry.Property();
+    }
+
+    @Override
+    public void startEntry() {
+        if (mCurrentVCardEntry != null) {
+            Log.e(LOG_TAG, "Nested VCard code is not supported now.");
+        }
+        mCurrentVCardEntry = new VCardEntry(mVCardType, mAccount);
+    }
+
+    @Override
+    public void endEntry() {
+        mCurrentVCardEntry.consolidateFields();
+        for (VCardEntryHandler entryHandler : mEntryHandlers) {
+            entryHandler.onEntryCreated(mCurrentVCardEntry);
+        }
+        mCurrentVCardEntry = null;
+    }
+
+    @Override
+    public void startProperty() {
+        mCurrentProperty.clear();
+    }
+
+    @Override
+    public void endProperty() {
+        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 " +
+                    "before propertyParamValue() is called");
+        }
+        mParamType = type;
+    }
+
+    @Override
+    public void propertyParamValue(String value) {
+        if (mParamType == null) {
+            // From vCard 2.1 specification. vCard 3.0 formally does not allow this case.
+            mParamType = "TYPE";
+        }
+        if (!VCardUtils.containsOnlyAlphaDigitHyphen(value)) {
+            value = VCardUtils.convertStringCharset(
+                    value, mSourceCharset, VCardConfig.DEFAULT_IMPORT_CHARSET);
+        }
+        mCurrentProperty.addParameter(mParamType, value);
+        mParamType = null;
+    }
+
+    private String handleOneValue(String value,
+            String sourceCharset, String targetCharset, String encoding) {
+        // It is possible when some of multiple values are empty.
+        // e.g. N:;a;;; -> values are "", "a", "", "", and "".
+        if (TextUtils.isEmpty(value)) {
+            return "";
+        }
+
+        if (encoding != null) {
+            if (encoding.equals("BASE64") || encoding.equals("B")) {
+                mCurrentProperty.setPropertyBytes(Base64.decode(value.getBytes(), Base64.DEFAULT));
+                return value;
+            } else if (encoding.equals("QUOTED-PRINTABLE")) {
+                return VCardUtils.parseQuotedPrintable(
+                        value, mStrictLineBreaking, sourceCharset, targetCharset);
+            }
+            Log.w(LOG_TAG, "Unknown encoding. Fall back to default.");
+        }
+
+        // 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) {
+        if (values == null || values.isEmpty()) {
+            return;
+        }
+
+        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 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, mSourceCharset, targetCharset, encoding));
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void showPerformanceInfo() {
+        Log.d(LOG_TAG, "time for insert ContactStruct to database: " + 
+                mTimePushIntoContentResolver + " ms");
+    }
+}
diff --git a/src/com/android/vcard/VCardEntryCounter.java b/src/com/android/vcard/VCardEntryCounter.java
new file mode 100644 (file)
index 0000000..7bfe977
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 com.android.vcard;
+
+import java.util.List;
+
+/**
+ * The class which just counts the number of vCard entries in the specified input.
+ */
+public class VCardEntryCounter implements VCardInterpreter {
+    private int mCount;
+
+    public int getCount() {
+        return mCount;
+    }
+
+    public void start() {
+    }
+
+    public void end() {
+    }
+
+    public void startEntry() {
+    }
+
+    public void endEntry() {
+        mCount++;
+    }
+
+    public void startProperty() {
+    }
+
+    public void endProperty() {
+    }
+
+    public void propertyGroup(String group) {
+    }
+
+    public void propertyName(String name) {
+    }
+
+    public void propertyParamType(String type) {
+    }
+
+    public void propertyParamValue(String value) {
+    }
+
+    public void propertyValues(List<String> values) {
+    }    
+}
diff --git a/src/com/android/vcard/VCardEntryHandler.java b/src/com/android/vcard/VCardEntryHandler.java
new file mode 100644 (file)
index 0000000..ef35a20
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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 com.android.vcard;
+
+/**
+ * <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 {
+    /**
+     * Called when the parsing started.
+     */
+    public void onStart();
+
+    /**
+     * The method called when one VCard entry is successfully created
+     */
+    public void onEntryCreated(final VCardEntry entry);
+
+    /**
+     * Called when the parsing ended.
+     * Able to be use this method for showing performance log, etc.
+     */
+    public void onEnd();
+}
diff --git a/src/com/android/vcard/VCardInterpreter.java b/src/com/android/vcard/VCardInterpreter.java
new file mode 100644 (file)
index 0000000..2d98764
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * 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 com.android.vcard;
+
+import java.util.List;
+
+/**
+ * <P>
+ * The interface which should be implemented by the classes which have to analyze each
+ * vCard entry minutely.
+ * </P>
+ * <P>
+ * Here, there are several terms specific to vCard (and this library).
+ * </P>
+ * <P>
+ * The term "entry" is one vCard representation in the input, which should start with "BEGIN:VCARD"
+ * and end with "END:VCARD".
+ * </P>
+ * <P>
+ * The term "property" is one line in vCard entry, which consists of "group", "property name",
+ * "parameter(param) names and values", and "property values".
+ * </P>
+ * <P>
+ * e.g. group1.propName;paramName1=paramValue1;paramName2=paramValue2;propertyValue1;propertyValue2...
+ * </P>
+ */
+public interface VCardInterpreter {
+    /**
+     * Called when vCard interpretation started.
+     */
+    void start();
+
+    /**
+     * Called when vCard interpretation finished.
+     */
+    void end();
+
+    /** 
+     * Called when parsing one vCard entry started.
+     * More specifically, this method is called when "BEGIN:VCARD" is read.
+     */
+    void startEntry();
+
+    /**
+     * Called when parsing one vCard entry ended.
+     * More specifically, this method is called when "END:VCARD" is read.
+     * Note that {@link #startEntry()} may be called since
+     * vCard (especially 2.1) allows nested vCard.
+     */
+    void endEntry();
+
+    /**
+     * Called when reading one property started.
+     */
+    void startProperty();
+
+    /**
+     * Called when reading one property ended.
+     */
+    void endProperty();
+
+    /**
+     * @param group A group name. This method may be called more than once or may not be
+     * called at all, depending on how many gruoups are appended to the property.
+     */
+    void propertyGroup(String group);
+
+    /**
+     * @param name A property name like "N", "FN", "ADR", etc.
+     */
+    void propertyName(String name);
+
+    /**
+     * @param type A parameter name like "ENCODING", "CHARSET", etc.
+     */
+    void propertyParamType(String type);
+
+    /**
+     * @param value A parameter value. This method may be called without
+     * {@link #propertyParamType(String)} being called (when the vCard is vCard 2.1).
+     */
+    void propertyParamValue(String value);
+
+    /**
+     * @param values List of property values. The size of values would be 1 unless
+     * coressponding property name is "N", "ADR", or "ORG".
+     */
+    void propertyValues(List<String> values);
+}
diff --git a/src/com/android/vcard/VCardInterpreterCollection.java b/src/com/android/vcard/VCardInterpreterCollection.java
new file mode 100644 (file)
index 0000000..adf577e
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * 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 com.android.vcard;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * The {@link VCardInterpreter} implementation which aggregates more than one
+ * {@link VCardInterpreter} objects and make a user object treat them as one
+ * {@link VCardInterpreter} object.
+ */
+public final class VCardInterpreterCollection implements VCardInterpreter {
+    private final Collection<VCardInterpreter> mInterpreterCollection;
+
+    public VCardInterpreterCollection(Collection<VCardInterpreter> interpreterCollection) {
+        mInterpreterCollection = interpreterCollection;
+    }
+
+    public Collection<VCardInterpreter> getCollection() {
+        return mInterpreterCollection;
+    }
+
+    public void start() {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.start();
+        }
+    }
+
+    public void end() {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.end();
+        }
+    }
+
+    public void startEntry() {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.startEntry();
+        }
+    }
+
+    public void endEntry() {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.endEntry();
+        }
+    }
+
+    public void startProperty() {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.startProperty();
+        }
+    }
+
+    public void endProperty() {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.endProperty();
+        }
+    }
+
+    public void propertyGroup(String group) {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.propertyGroup(group);
+        }
+    }
+
+    public void propertyName(String name) {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.propertyName(name);
+        }
+    }
+
+    public void propertyParamType(String type) {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.propertyParamType(type);
+        }
+    }
+
+    public void propertyParamValue(String value) {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.propertyParamValue(value);
+        }
+    }
+
+    public void propertyValues(List<String> values) {
+        for (VCardInterpreter builder : mInterpreterCollection) {
+            builder.propertyValues(values);
+        }
+    }
+}
diff --git a/src/com/android/vcard/VCardParser.java b/src/com/android/vcard/VCardParser.java
new file mode 100644 (file)
index 0000000..b7b8291
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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 com.android.vcard;
+
+import com.android.vcard.exception.VCardException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface VCardParser {
+    /**
+     * <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 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.
+     * @throws IOException, VCardException
+     */
+    public void parse(InputStream is, VCardInterpreter interepreter)
+            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 void cancel();
+}
diff --git a/src/com/android/vcard/VCardParserImpl_V21.java b/src/com/android/vcard/VCardParserImpl_V21.java
new file mode 100644 (file)
index 0000000..f030c6e
--- /dev/null
@@ -0,0 +1,1047 @@
+/*
+ * 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 com.android.vcard;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.vcard.exception.VCardAgentNotSupportedException;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardInvalidCommentLineException;
+import com.android.vcard.exception.VCardInvalidLineException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardVersionException;
+
+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/src/com/android/vcard/VCardParserImpl_V30.java b/src/com/android/vcard/VCardParserImpl_V30.java
new file mode 100644 (file)
index 0000000..648ce7e
--- /dev/null
@@ -0,0 +1,389 @@
+/*
+ * 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 com.android.vcard;
+
+import android.util.Log;
+
+import com.android.vcard.exception.VCardException;
+
+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/src/com/android/vcard/VCardParserImpl_V40.java b/src/com/android/vcard/VCardParserImpl_V40.java
new file mode 100644 (file)
index 0000000..3a49444
--- /dev/null
@@ -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 com.android.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/src/com/android/vcard/VCardParser_V21.java b/src/com/android/vcard/VCardParser_V21.java
new file mode 100644 (file)
index 0000000..7aa7a82
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * 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 com.android.vcard;
+
+import com.android.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 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 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.
+     * </p>
+     */
+    /* 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)));
+
+    private final VCardParserImpl_V21 mVCardParserImpl;
+
+    public VCardParser_V21() {
+        mVCardParserImpl = new VCardParserImpl_V21();
+    }
+
+    public VCardParser_V21(int vcardType) {
+        mVCardParserImpl = new VCardParserImpl_V21(vcardType);
+    }
+
+    public void parse(InputStream is, VCardInterpreter interepreter)
+            throws IOException, VCardException {
+        mVCardParserImpl.parse(is, interepreter);
+    }
+
+    public void cancel() {
+        mVCardParserImpl.cancel();
+    }
+}
diff --git a/src/com/android/vcard/VCardParser_V30.java b/src/com/android/vcard/VCardParser_V30.java
new file mode 100644 (file)
index 0000000..711a00e
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * 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 com.android.vcard;
+
+import com.android.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 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 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
+                    "IMPP"))); // RFC 4770
+
+    /**
+     * <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>
+     */
+    /* 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() {
+        mVCardParserImpl = new VCardParserImpl_V30();
+    }
+
+    public VCardParser_V30(int vcardType) {
+        mVCardParserImpl = new VCardParserImpl_V30(vcardType);
+    }
+
+    public void parse(InputStream is, VCardInterpreter interepreter)
+            throws IOException, VCardException {
+        mVCardParserImpl.parse(is, interepreter);
+    }
+
+    public void cancel() {
+        mVCardParserImpl.cancel();
+    }
+}
diff --git a/src/com/android/vcard/VCardParser_V40.java b/src/com/android/vcard/VCardParser_V40.java
new file mode 100644 (file)
index 0000000..700cdfd
--- /dev/null
@@ -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 com.android.vcard;
+
+import com.android.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/src/com/android/vcard/VCardSourceDetector.java b/src/com/android/vcard/VCardSourceDetector.java
new file mode 100644 (file)
index 0000000..c3e8ca8
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * 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 com.android.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;
+
+/**
+ * <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"));
+    
+    private static Set<String> JAPANESE_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList(
+            "X-GNO", "X-GN", "X-REDUCTION"));
+    
+    private static Set<String> WINDOWS_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList(
+            "X-MICROSOFT-ASST_TEL", "X-MICROSOFT-ASSISTANT", "X-MICROSOFT-OFFICELOC"));
+    
+    // Note: these signes appears before the signs of the other type (e.g. "X-GN").
+    // In other words, Japanese FOMA mobile phones are detected as FOMA, not JAPANESE_MOBILE_PHONES.
+    private static Set<String> FOMA_SIGNS = new HashSet<String>(Arrays.asList(
+            "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";
+
+    /**
+     * 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 mNeedToParseCharset;
+    private String mSpecifiedCharset;
+    
+    public void start() {
+    }
+    
+    public void end() {
+    }
+
+    public void startEntry() {
+    }    
+
+    public void startProperty() {
+        mNeedToParseCharset = false;
+        mNeedToParseVersion = false;
+    }
+    
+    public void endProperty() {
+    }
+
+    public void endEntry() {
+    }
+
+    public void propertyGroup(String group) {
+    }
+    
+    public void propertyName(String name) {
+        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 (mParseType != PARSE_TYPE_UNKNOWN) {
+            return;
+        }
+        if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) {
+            mParseType = PARSE_TYPE_WINDOWS_MOBILE_V65_JP;
+        } else if (FOMA_SIGNS.contains(name)) {
+            mParseType = PARSE_TYPE_DOCOMO_TORELATE_NEST;
+        } else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) {
+            mParseType = PARSE_TYPE_MOBILE_PHONE_JP;
+        } else if (APPLE_SIGNS.contains(name)) {
+            mParseType = PARSE_TYPE_APPLE;
+        }
+    }
+
+    public void propertyParamType(String type) {
+    }
+
+    public void propertyParamValue(String value) {
+    }
+
+    public void propertyValues(List<String> values) {
+        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);
+        }
+    }
+
+    /**
+     * @return The available type can be used with vCard parser. You probably need to
+     * use {{@link #getEstimatedCharset()} to understand the charset to be used.
+     */