diff options
Diffstat (limited to 'vcard/java/com/android')
26 files changed, 8320 insertions, 0 deletions
diff --git a/vcard/java/com/android/vcard/JapaneseUtils.java b/vcard/java/com/android/vcard/JapaneseUtils.java new file mode 100644 index 0000000..5b44944 --- /dev/null +++ b/vcard/java/com/android/vcard/JapaneseUtils.java @@ -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/vcard/java/com/android/vcard/VCardBuilder.java b/vcard/java/com/android/vcard/VCardBuilder.java new file mode 100644 index 0000000..6ef9ada --- /dev/null +++ b/vcard/java/com/android/vcard/VCardBuilder.java @@ -0,0 +1,1996 @@ +/* + * 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.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_V30 = + "ENCODING=" + VCardConstants.PARAM_ENCODING_B; + + private static final String SHIFT_JIS = "SHIFT_JIS"; + + private final int mVCardType; + + private final boolean mIsV30; + 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. + */ + public VCardBuilder(final int vcardType, String charset) { + mVCardType = vcardType; + + mIsV30 = VCardConfig.isV30(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 = !(mIsV30 && "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 (mIsV30) { + appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30); + } else { + 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; + } + + /** + * 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 (contentValuesList == null || contentValuesList.isEmpty()) { + if (mIsDoCoMo) { + appendLine(VCardConstants.PROPERTY_N, ""); + } else if (mIsV30) { + // vCard 3.0 requires "N" and "FN" properties. + appendLine(VCardConstants.PROPERTY_N, ""); + appendLine(VCardConstants.PROPERTY_FN, ""); + } + 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); + + 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); + 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 (mIsV30) { + // vCard 3.0 specification requires these fields. + appendLine(VCardConstants.PROPERTY_N, ""); + appendLine(VCardConstants.PROPERTY_FN, ""); + } else if (mIsDoCoMo) { + appendLine(VCardConstants.PROPERTY_N, ""); + } + + appendPhoneticNameFields(contentValues); + return this; + } + + 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; + } + + // Try to emit the field(s) related to phonetic name. + if (mIsV30) { + final String sortString = VCardUtils + .constructNameFromElements(mVCardType, + phoneticFamilyName, phoneticMiddleName, phoneticGivenName); + mBuilder.append(VCardConstants.PROPERTY_SORT_STRING); + if (shouldAppendCharsetParam(sortString)) { + 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 (mIsV30) { + 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". + 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 format = VCardUtils.getPhoneNumberFormat(mVCardType); + final String formattedPhoneNumber = + PhoneNumberUtils.formatNumber(actualPhoneNumber, format); + phoneSet.add(actualPhoneNumber); + appendTelLine(type, label, formattedPhoneNumber, isPrimary); + } + } // for (String actualPhoneNumber : phoneNumberList) { + } + } + } + + 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 { + 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 (mIsV30) { + tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30); + } 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); + } + + 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) { + // Note: vCard 3.0 specifies the different type of acceptable type Strings, but + // we don't emit that kind of vCard 3.0 specific type since there should be + // high probabilyty in which external importers cannot understand them. + // + // e.g. TYPE="\u578B\u306B\u3087" (vCard 3.0 allows non-Ascii characters if they + // are quoted.) + 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 ((mIsV30 || 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 (mIsV30) { + tmpBuilder.append("\\\\"); + break; + } else { + // fall through + } + } + case '<': + case '>': { + if (mIsDoCoMo) { + tmpBuilder.append('\\'); + tmpBuilder.append(ch); + } else { + tmpBuilder.append(ch); + } + break; + } + case ',': { + if (mIsV30) { + 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/vcard/java/com/android/vcard/VCardComposer.java b/vcard/java/com/android/vcard/VCardComposer.java new file mode 100644 index 0000000..7038955 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardComposer.java @@ -0,0 +1,677 @@ +/* + * 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.content.ContentValues; +import android.content.Context; +import android.content.Entity; +import android.content.EntityIterator; +import android.content.Entity.NamedContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.net.Uri; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import android.provider.ContactsContract.RawContactsEntity; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Im; +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.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.text.TextUtils; +import android.util.CharsetUtils; +import android.util.Log; + +import com.android.vcard.exception.VCardException; + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * <p> + * The class for composing vCard from Contacts information. + * </p> + * <p> + * Usually, this class should be used like this. + * </p> + * <pre class="prettyprint">VCardComposer composer = null; + * try { + * composer = new VCardComposer(context); + * composer.addHandler( + * composer.new HandlerForOutputStream(outputStream)); + * if (!composer.init()) { + * // Do something handling the situation. + * return; + * } + * while (!composer.isAfterLast()) { + * if (mCanceled) { + * // Assume a user may cancel this operation during the export. + * return; + * } + * if (!composer.createOneEntry()) { + * // Do something handling the error situation. + * return; + * } + * } + * } finally { + * if (composer != null) { + * composer.terminate(); + * } + * }</pre> + * <p> + * Users have to manually take care of memory efficiency. Even one vCard may contain + * image of non-trivial size for mobile devices. + * </p> + * <p> + * {@link VCardBuilder} is used to build each vCard. + * </p> + */ +public class VCardComposer { + private static final String LOG_TAG = "VCardComposer"; + + public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = + "Failed to get database information"; + + public static final String FAILURE_REASON_NO_ENTRY = + "There's no exportable in the database"; + + public static final String FAILURE_REASON_NOT_INITIALIZED = + "The vCard composer object is not correctly initialized"; + + /** Should be visible only from developers... (no need to translate, hopefully) */ + public static final String FAILURE_REASON_UNSUPPORTED_URI = + "The Uri vCard composer received is not supported by the composer."; + + public static final String NO_ERROR = "No error"; + + public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; + + // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here, + // since usual vCard devices for Japanese devices already use it. + private static final String SHIFT_JIS = "SHIFT_JIS"; + private static final String UTF_8 = "UTF-8"; + + /** + * Special URI for testing. + */ + public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard"; + public static final Uri VCARD_TEST_AUTHORITY_URI = + Uri.parse("content://" + VCARD_TEST_AUTHORITY); + public static final Uri CONTACTS_TEST_CONTENT_URI = + Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts"); + + private static final Map<Integer, String> sImMap; + + static { + sImMap = new HashMap<Integer, String>(); + sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); + sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); + sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); + sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); + sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); + sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); + // We don't add Google talk here since it has to be handled separately. + } + + public static interface OneEntryHandler { + public boolean onInit(Context context); + public boolean onEntryCreated(String vcard); + public void onTerminate(); + } + + /** + * <p> + * An useful handler for emitting vCard String to an OutputStream object one by one. + * </p> + * <p> + * The input OutputStream object is closed() on {@link #onTerminate()}. + * Must not close the stream outside this class. + * </p> + */ + public final class HandlerForOutputStream implements OneEntryHandler { + @SuppressWarnings("hiding") + private static final String LOG_TAG = "VCardComposer.HandlerForOutputStream"; + + private boolean mOnTerminateIsCalled = false; + + private final OutputStream mOutputStream; // mWriter will close this. + private Writer mWriter; + + /** + * Input stream will be closed on the detruction of this object. + */ + public HandlerForOutputStream(final OutputStream outputStream) { + mOutputStream = outputStream; + } + + public boolean onInit(final Context context) { + try { + mWriter = new BufferedWriter(new OutputStreamWriter( + mOutputStream, mCharset)); + } catch (UnsupportedEncodingException e1) { + Log.e(LOG_TAG, "Unsupported charset: " + mCharset); + mErrorReason = "Encoding is not supported (usually this does not happen!): " + + mCharset; + return false; + } + + if (mIsDoCoMo) { + try { + // Create one empty entry. + mWriter.write(createOneEntryInternal("-1", null)); + } catch (VCardException e) { + Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " + + e.getMessage()); + return false; + } catch (IOException e) { + Log.e(LOG_TAG, + "IOException occurred during exportOneContactData: " + + e.getMessage()); + mErrorReason = "IOException occurred: " + e.getMessage(); + return false; + } + } + return true; + } + + public boolean onEntryCreated(String vcard) { + try { + mWriter.write(vcard); + } catch (IOException e) { + Log.e(LOG_TAG, + "IOException occurred during exportOneContactData: " + + e.getMessage()); + mErrorReason = "IOException occurred: " + e.getMessage(); + return false; + } + return true; + } + + public void onTerminate() { + mOnTerminateIsCalled = true; + if (mWriter != null) { + try { + // Flush and sync the data so that a user is able to pull + // the SDCard just after + // the export. + mWriter.flush(); + if (mOutputStream != null + && mOutputStream instanceof FileOutputStream) { + ((FileOutputStream) mOutputStream).getFD().sync(); + } + } catch (IOException e) { + Log.d(LOG_TAG, + "IOException during closing the output stream: " + + e.getMessage()); + } finally { + closeOutputStream(); + } + } + } + + public void closeOutputStream() { + try { + mWriter.close(); + } catch (IOException e) { + Log.w(LOG_TAG, "IOException is thrown during close(). Ignoring."); + } + } + + @Override + public void finalize() { + if (!mOnTerminateIsCalled) { + onTerminate(); + } + } + } + + private final Context mContext; + private final int mVCardType; + private final boolean mCareHandlerErrors; + private final ContentResolver mContentResolver; + + private final boolean mIsDoCoMo; + private Cursor mCursor; + private int mIdColumn; + + private final String mCharset; + private boolean mTerminateIsCalled; + private final List<OneEntryHandler> mHandlerList; + + private String mErrorReason = NO_ERROR; + + private static final String[] sContactsProjection = new String[] { + Contacts._ID, + }; + + public VCardComposer(Context context) { + this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true); + } + + /** + * The variant which sets charset to null and sets careHandlerErrors to true. + */ + public VCardComposer(Context context, int vcardType) { + this(context, vcardType, null, true); + } + + public VCardComposer(Context context, int vcardType, String charset) { + this(context, vcardType, charset, true); + } + + /** + * The variant which sets charset to null. + */ + public VCardComposer(final Context context, final int vcardType, + final boolean careHandlerErrors) { + this(context, vcardType, null, careHandlerErrors); + } + + /** + * Construct for supporting call log entry vCard composing. + * + * @param context Context to be used during the composition. + * @param vcardType The type of vCard, typically available via {@link VCardConfig}. + * @param charset The charset to be used. Use null when you don't need the charset. + * @param careHandlerErrors If true, This object returns false everytime + * a Handler object given via {{@link #addHandler(OneEntryHandler)} returns false. + * If false, this ignores those errors. + */ + public VCardComposer(final Context context, final int vcardType, String charset, + final boolean careHandlerErrors) { + mContext = context; + mVCardType = vcardType; + mCareHandlerErrors = careHandlerErrors; + mContentResolver = context.getContentResolver(); + + mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); + mHandlerList = new ArrayList<OneEntryHandler>(); + + charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset); + final boolean shouldAppendCharsetParam = !( + VCardConfig.isV30(vcardType) && UTF_8.equalsIgnoreCase(charset)); + + if (mIsDoCoMo || shouldAppendCharsetParam) { + if (SHIFT_JIS.equalsIgnoreCase(charset)) { + if (mIsDoCoMo) { + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, + "DoCoMo-specific SHIFT_JIS was not found. " + + "Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + } else { + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, + "Career-specific SHIFT_JIS was not found. " + + "Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + } + mCharset = charset; + } else { + Log.w(LOG_TAG, + "The charset \"" + charset + "\" is used while " + + SHIFT_JIS + " is needed to be used."); + if (TextUtils.isEmpty(charset)) { + mCharset = SHIFT_JIS; + } else { + try { + charset = CharsetUtils.charsetForVendor(charset).name(); + } catch (UnsupportedCharsetException e) { + Log.i(LOG_TAG, + "Career-specific \"" + charset + "\" was not found (as usual). " + + "Use it as is."); + } + mCharset = charset; + } + } + } else { + if (TextUtils.isEmpty(charset)) { + mCharset = UTF_8; + } else { + try { + charset = CharsetUtils.charsetForVendor(charset).name(); + } catch (UnsupportedCharsetException e) { + Log.i(LOG_TAG, + "Career-specific \"" + charset + "\" was not found (as usual). " + + "Use it as is."); + } + mCharset = charset; + } + } + + Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\""); + } + + /** + * Must be called before {@link #init()}. + */ + public void addHandler(OneEntryHandler handler) { + if (handler != null) { + mHandlerList.add(handler); + } + } + + /** + * @return Returns true when initialization is successful and all the other + * methods are available. Returns false otherwise. + */ + public boolean init() { + return init(null, null); + } + + public boolean init(final String selection, final String[] selectionArgs) { + return init(Contacts.CONTENT_URI, selection, selectionArgs, null); + } + + /** + * Note that this is unstable interface, may be deleted in the future. + */ + public boolean init(final Uri contentUri, final String selection, + final String[] selectionArgs, final String sortOrder) { + if (contentUri == null) { + return false; + } + + if (mCareHandlerErrors) { + final List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( + mHandlerList.size()); + for (OneEntryHandler handler : mHandlerList) { + if (!handler.onInit(mContext)) { + for (OneEntryHandler finished : finishedList) { + finished.onTerminate(); + } + return false; + } + } + } else { + // Just ignore the false returned from onInit(). + for (OneEntryHandler handler : mHandlerList) { + handler.onInit(mContext); + } + } + + final String[] projection; + if (Contacts.CONTENT_URI.equals(contentUri) || + CONTACTS_TEST_CONTENT_URI.equals(contentUri)) { + projection = sContactsProjection; + } else { + mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; + return false; + } + mCursor = mContentResolver.query( + contentUri, projection, selection, selectionArgs, sortOrder); + + if (mCursor == null) { + mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; + return false; + } + + if (getCount() == 0 || !mCursor.moveToFirst()) { + try { + mCursor.close(); + } catch (SQLiteException e) { + Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); + } finally { + mCursor = null; + mErrorReason = FAILURE_REASON_NO_ENTRY; + } + return false; + } + + mIdColumn = mCursor.getColumnIndex(Contacts._ID); + + return true; + } + + public boolean createOneEntry() { + return createOneEntry(null); + } + + /** + * @param getEntityIteratorMethod For Dependency Injection. + * @hide just for testing. + */ + public boolean createOneEntry(Method getEntityIteratorMethod) { + if (mCursor == null || mCursor.isAfterLast()) { + mErrorReason = FAILURE_REASON_NOT_INITIALIZED; + return false; + } + final String vcard; + try { + if (mIdColumn >= 0) { + vcard = createOneEntryInternal(mCursor.getString(mIdColumn), + getEntityIteratorMethod); + } else { + Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); + return true; + } + } catch (VCardException e) { + Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage()); + return false; + } catch (OutOfMemoryError error) { + // Maybe some data (e.g. photo) is too big to have in memory. But it + // should be rare. + Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry."); + System.gc(); + // TODO: should tell users what happened? + return true; + } finally { + mCursor.moveToNext(); + } + + // This function does not care the OutOfMemoryError on the handler side :-P + if (mCareHandlerErrors) { + List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( + mHandlerList.size()); + for (OneEntryHandler handler : mHandlerList) { + if (!handler.onEntryCreated(vcard)) { + return false; + } + } + } else { + for (OneEntryHandler handler : mHandlerList) { + handler.onEntryCreated(vcard); + } + } + + return true; + } + + private String createOneEntryInternal(final String contactId, + final Method getEntityIteratorMethod) throws VCardException { + final Map<String, List<ContentValues>> contentValuesListMap = + new HashMap<String, List<ContentValues>>(); + // The resolver may return the entity iterator with no data. It is possible. + // e.g. If all the data in the contact of the given contact id are not exportable ones, + // they are hidden from the view of this method, though contact id itself exists. + EntityIterator entityIterator = null; + try { + final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon() + // .appendQueryParameter("for_export_only", "1") + .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") + .build(); + final String selection = Data.CONTACT_ID + "=?"; + final String[] selectionArgs = new String[] {contactId}; + if (getEntityIteratorMethod != null) { + // Please note that this branch is executed by unit tests only + try { + entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, + mContentResolver, uri, selection, selectionArgs, null); + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " + + e.getMessage()); + } catch (IllegalAccessException e) { + Log.e(LOG_TAG, "IllegalAccessException has been thrown: " + + e.getMessage()); + } catch (InvocationTargetException e) { + Log.e(LOG_TAG, "InvocationTargetException has been thrown: "); + StackTraceElement[] stackTraceElements = e.getCause().getStackTrace(); + for (StackTraceElement element : stackTraceElements) { + Log.e(LOG_TAG, " at " + element.toString()); + } + throw new VCardException("InvocationTargetException has been thrown: " + + e.getCause().getMessage()); + } + } else { + entityIterator = RawContacts.newEntityIterator(mContentResolver.query( + uri, null, selection, selectionArgs, null)); + } + + if (entityIterator == null) { + Log.e(LOG_TAG, "EntityIterator is null"); + return ""; + } + + if (!entityIterator.hasNext()) { + Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId); + return ""; + } + + while (entityIterator.hasNext()) { + Entity entity = entityIterator.next(); + for (NamedContentValues namedContentValues : entity.getSubValues()) { + ContentValues contentValues = namedContentValues.values; + String key = contentValues.getAsString(Data.MIMETYPE); + if (key != null) { + List<ContentValues> contentValuesList = + contentValuesListMap.get(key); + if (contentValuesList == null) { + contentValuesList = new ArrayList<ContentValues>(); + contentValuesListMap.put(key, contentValuesList); + } + contentValuesList.add(contentValues); + } + } + } + } finally { + if (entityIterator != null) { + entityIterator.close(); + } + } + + return buildVCard(contentValuesListMap); + } + + /** + * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in + * {ContactsContract}. Developers can override this method to customize the output. + */ + public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) { + if (contentValuesListMap == null) { + Log.e(LOG_TAG, "The given map is null. Ignore and return empty String"); + return ""; + } else { + final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset); + builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) + .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) + .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) + .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) + .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) + .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) + .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)) + .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)) + .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) + .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) + .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) + .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); + return builder.toString(); + } + } + + public void terminate() { + for (OneEntryHandler handler : mHandlerList) { + handler.onTerminate(); + } + + if (mCursor != null) { + try { + mCursor.close(); + } catch (SQLiteException e) { + Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); + } + mCursor = null; + } + + mTerminateIsCalled = true; + } + + @Override + public void finalize() { + if (!mTerminateIsCalled) { + Log.w(LOG_TAG, "terminate() is not called yet. We call it in finalize() step."); + terminate(); + } + } + + /** + * @return returns the number of available entities. The return value is undefined + * when this object is not ready yet (typically when {{@link #init()} is not called + * or when {@link #terminate()} is already called). + */ + public int getCount() { + if (mCursor == null) { + Log.w(LOG_TAG, "This object is not ready yet."); + return 0; + } + return mCursor.getCount(); + } + + /** + * @return true when there's no entity to be built. The return value is undefined + * when this object is not ready yet. + */ + public boolean isAfterLast() { + if (mCursor == null) { + Log.w(LOG_TAG, "This object is not ready yet."); + return false; + } + return mCursor.isAfterLast(); + } + + /** + * @return Returns the error reason. + */ + public String getErrorReason() { + return mErrorReason; + } +} diff --git a/vcard/java/com/android/vcard/VCardConfig.java b/vcard/java/com/android/vcard/VCardConfig.java new file mode 100644 index 0000000..fc95922 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardConfig.java @@ -0,0 +1,478 @@ +/* + * 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 a given vCard file + * at first, while we have to decode sime encoded data (e.g. BASE64) to binary. + * In order to avoid "misinterpretation" of charset as much as possible, + * "ISO-8859-1" (a.k.a Latin-1) is first used for reading a stream. + * When charset is specified in a property (with "CHARSET=..." parameter), + * the string is decoded to raw bytes and encoded into the specific charset, + * assuming "ISO-8859-1" is able to map "all" 8bit characters to some unicode, + * and it has 1 to 1 mapping in all 8bit characters. + * If the assumption is not correct, this setting will cause some bug. + * </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"; + + public static final int FLAG_V21 = 0; + public static final int FLAG_V30 = 1; + + // 0x2 is reserved for the future use ... + + 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; + + //// 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 = + (FLAG_V21 | 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 = + (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V30_GENERIC_STR = "v30_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 = + (FLAG_V21 | 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 = + (FLAG_V30 | 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 = + (FLAG_V21 | 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 = + (FLAG_V30 | 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 = + (FLAG_V21 | 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 isV30(final int vcardType) { + return ((vcardType & FLAG_V30) != 0); + } + + public static boolean shouldUseQuotedPrintable(final int vcardType) { + return !isV30(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 (isV30(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/vcard/java/com/android/vcard/VCardConstants.java b/vcard/java/com/android/vcard/VCardConstants.java new file mode 100644 index 0000000..862c9edc --- /dev/null +++ b/vcard/java/com/android/vcard/VCardConstants.java @@ -0,0 +1,160 @@ +/* + * 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"; + + // 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 + public static final String PROPERTY_END = "END"; + + // Valid property names not supported (not appropriately handled) by our vCard importer now. + public static final String PROPERTY_REV = "REV"; + public static final String PROPERTY_AGENT = "AGENT"; + + // Available in vCard 3.0. Shoud not use when composing vCard 2.1 file. + public static final String PROPERTY_NAME = "NAME"; + public static final String PROPERTY_NICKNAME = "NICKNAME"; + public static final String PROPERTY_SORT_STRING = "SORT-STRING"; + + // De-fact property values expressing phonetic names. + public static final String PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; + public static final String PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; + public static final String PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; + + // Properties both ContactsStruct in Eclair and de-fact vCard extensions + // shown in http://en.wikipedia.org/wiki/VCard support are defined here. + 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"; + + // 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"; + + // 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"; + + /* package */ 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/vcard/java/com/android/vcard/VCardEntry.java b/vcard/java/com/android/vcard/VCardEntry.java new file mode 100644 index 0000000..624407a --- /dev/null +++ b/vcard/java/com/android/vcard/VCardEntry.java @@ -0,0 +1,1423 @@ +/* + * 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.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +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.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +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.List; +import java.util.Map; + +/** + * 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 boolean isPrimary; + + public OrganizationData(int type, + String companyName, + String departmentName, + String titleName, + boolean isPrimary) { + this.type = type; + this.companyName = companyName; + this.departmentName = departmentName; + this.titleName = titleName; + 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; + } + } + + 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 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 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, boolean)} or + * {@link #handleTitleValue(String)}. + */ + private void addNewOrganization(int type, final String companyName, + final String departmentName, + final String titleName, boolean isPrimary) { + if (mOrganizationList == null) { + mOrganizationList = new ArrayList<OrganizationData>(); + } + mOrganizationList.add(new OrganizationData(type, companyName, + departmentName, titleName, isPrimary)); + } + + private static final List<String> sEmptyList = + Collections.unmodifiableList(new ArrayList<String>(0)); + + /** + * 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, boolean isPrimary) { + 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, 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, 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, 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, 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); + } + + @SuppressWarnings("fallthrough") + private void handleNProperty(List<String> elems) { + // Family, Given, Middle, Prefix, Suffix. (1 - 5) + int size; + if (elems == null || (size = elems.size()) < 1) { + return; + } + if (size > 5) { + size = 5; + } + + switch (size) { + // fallthrough + case 5: mSuffix = elems.get(4); + case 4: mPrefix = elems.get(3); + case 3: mMiddleName = elems.get(2); + case 2: mGivenName = elems.get(1); + default: mFamilyName = elems.get(0); + } + } + + /** + * 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); + } 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, + VCardConfig.isV30(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, 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 Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); + final Object typeObject = + VCardUtils.getPhoneTypeFromStrings(typeCollection, propValue); + 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, propValue, 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 = Phone.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_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_X_ANDROID_CUSTOM)) { + final List<String> customPropertyList = + VCardUtils.constructListFromValue(propValue, + VCardConfig.isV30(mVCardType)); + handleAndroidCustomProperty(customPropertyList); + /*} else if (propName.equals("REV")) { + // Revision of this VCard entry. I think we can ignore this. + } else if (propName.equals("UID")) { + } else if (propName.equals("KEY")) { + // Type is X509 or PGP? I don't know how to handle this... + } else if (propName.equals("MAILER")) { + } else if (propName.equals("TZ")) { + } else if (propName.equals("GEO")) { + } else if (propName.equals("CLASS")) { + // vCard 3.0 only. + // e.g. CLASS:CONFIDENTIAL + } else if (propName.equals("PROFILE")) { + // VCard 3.0 only. Must be "VCARD". I think we can ignore this. + } else if (propName.equals("CATEGORIES")) { + // VCard 3.0 only. + // e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY + } else if (propName.equals("SOURCE")) { + // VCard 3.0 only. + } else if (propName.equals("PRODID")) { + // VCard 3.0 only. + // To specify the identifier for the product that created + // the vCard object.*/ + } else { + // Unknown X- words and IANA token. + } + } + + 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.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); + 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); + if (imData.protocol == Im.PROTOCOL_CUSTOM) { + builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol); + } + if (imData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); + } + } + } + + 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 (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/vcard/java/com/android/vcard/VCardEntryCommitter.java b/vcard/java/com/android/vcard/VCardEntryCommitter.java new file mode 100644 index 0000000..7bd314e --- /dev/null +++ b/vcard/java/com/android/vcard/VCardEntryCommitter.java @@ -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/vcard/java/com/android/vcard/VCardEntryConstructor.java b/vcard/java/com/android/vcard/VCardEntryConstructor.java new file mode 100644 index 0000000..2679e23 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardEntryConstructor.java @@ -0,0 +1,240 @@ +/* + * 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.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * <p> + * The {@link VCardInterpreter} implementation which enables {@link VCardEntryHandler} objects + * to easily handle each vCard entry. + * </p> + * <p> + * This class understand details inside vCard and translates it to {@link VCardEntry}. + * Then the class throw it to {@link VCardEntryHandler} registered via + * {@link #addEntryHandler(VCardEntryHandler)}, so that all those registered objects + * are able to handle the {@link VCardEntry} object. + * </p> + * <p> + * If you want to know the detail inside vCard, it would be better to implement + * {@link VCardInterpreter} directly, instead of relying on this class and + * {@link VCardEntry} created by the object. + * </p> + */ +public class VCardEntryConstructor implements VCardInterpreter { + private static String LOG_TAG = "VCardEntryConstructor"; + + 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, null, false); + } + + 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 + */ + 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); + } + + public void start() { + for (VCardEntryHandler entryHandler : mEntryHandlers) { + entryHandler.onStart(); + } + } + + public void end() { + for (VCardEntryHandler entryHandler : mEntryHandlers) { + entryHandler.onEnd(); + } + } + + public void clear() { + mCurrentVCardEntry = null; + mCurrentProperty = new VCardEntry.Property(); + } + + public void startEntry() { + if (mCurrentVCardEntry != null) { + Log.e(LOG_TAG, "Nested VCard code is not supported now."); + } + mCurrentVCardEntry = new VCardEntry(mVCardType, mAccount); + } + + public void endEntry() { + mCurrentVCardEntry.consolidateFields(); + for (VCardEntryHandler entryHandler : mEntryHandlers) { + entryHandler.onEntryCreated(mCurrentVCardEntry); + } + mCurrentVCardEntry = null; + } + + public void startProperty() { + mCurrentProperty.clear(); + } + + public void endProperty() { + mCurrentVCardEntry.addProperty(mCurrentProperty); + } + + public void propertyName(String name) { + mCurrentProperty.setPropertyName(name); + } + + public void propertyGroup(String group) { + } + + public void propertyParamType(String type) { + if (mParamType != null) { + Log.e(LOG_TAG, "propertyParamType() is called more than once " + + "before propertyParamValue() is called"); + } + mParamType = type; + } + + public void propertyParamValue(String value) { + if (mParamType == null) { + // From vCard 2.1 specification. vCard 3.0 formally does not allow this case. + mParamType = "TYPE"; + } + mCurrentProperty.addParameter(mParamType, value); + mParamType = null; + } + + private static String encodeToSystemCharset(String originalString, + String sourceCharset, String targetCharset) { + if (sourceCharset.equalsIgnoreCase(targetCharset)) { + return originalString; + } + final Charset charset = Charset.forName(sourceCharset); + final ByteBuffer byteBuffer = charset.encode(originalString); + // byteBuffer.array() "may" return byte array which is larger than + // byteBuffer.remaining(). Here, we keep on the safe side. + final byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + try { + String ret = new String(bytes, targetCharset); + return ret; + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return null; + } + } + + private String handleOneValue(String value, + String sourceCharset, String targetCharset, String encoding) { + if (value == null) { + Log.w(LOG_TAG, "Null is given."); + value = ""; + } + + 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 encodeToSystemCharset(value, sourceCharset, targetCharset); + } + + public void propertyValues(List<String> values) { + if (values == null || values.isEmpty()) { + return; + } + + final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET"); + final Collection<String> encodingCollection = mCurrentProperty.getParameters("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/vcard/java/com/android/vcard/VCardEntryCounter.java b/vcard/java/com/android/vcard/VCardEntryCounter.java new file mode 100644 index 0000000..7bfe977 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardEntryCounter.java @@ -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/vcard/java/com/android/vcard/VCardEntryHandler.java b/vcard/java/com/android/vcard/VCardEntryHandler.java new file mode 100644 index 0000000..ef35a20 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardEntryHandler.java @@ -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/vcard/java/com/android/vcard/VCardInterpreter.java b/vcard/java/com/android/vcard/VCardInterpreter.java new file mode 100644 index 0000000..2d98764 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardInterpreter.java @@ -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/vcard/java/com/android/vcard/VCardInterpreterCollection.java b/vcard/java/com/android/vcard/VCardInterpreterCollection.java new file mode 100644 index 0000000..4a40d93 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardInterpreterCollection.java @@ -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/vcard/java/com/android/vcard/VCardParser.java b/vcard/java/com/android/vcard/VCardParser.java new file mode 100644 index 0000000..b7b8291 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardParser.java @@ -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/vcard/java/com/android/vcard/VCardParserImpl_V21.java b/vcard/java/com/android/vcard/VCardParserImpl_V21.java new file mode 100644 index 0000000..00ae6c9 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardParserImpl_V21.java @@ -0,0 +1,968 @@ +/* + * 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.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 CustomBufferedReader extends BufferedReader { + private long mTime; + + public CustomBufferedReader(Reader in) { + super(in); + } + + @Override + public String readLine() throws IOException { + long start = System.currentTimeMillis(); + String ret = super.readLine(); + long end = System.currentTimeMillis(); + mTime += end - start; + return ret; + } + + public long getTotalmillisecond() { + return mTime; + } + } + + private static final String sDefaultEncoding = "8BIT"; + + protected boolean mCanceled; + protected VCardInterpreter mInterpreter; + + protected final String mImportCharset; + + /** + * <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 BufferedReader 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, null); + } + + public VCardParserImpl_V21(int vcardType) { + this(vcardType, null); + } + + public VCardParserImpl_V21(int vcardType, String importCharset) { + if ((vcardType & VCardConfig.FLAG_TORELATE_NEST) != 0) { + mNestCount = 1; + } + + mImportCharset = (!TextUtils.isEmpty(importCharset) ? importCharset : + 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(); + } + + /** + * @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; + } + long start; + if (mInterpreter != null) { + start = System.currentTimeMillis(); + mInterpreter.startEntry(); + mTimeReadStartRecord += System.currentTimeMillis() - start; + } + start = System.currentTimeMillis(); + parseItems(); + mTimeParseItems += System.currentTimeMillis() - start; + readEndVCard(true, false); + if (mInterpreter != null) { + start = System.currentTimeMillis(); + mInterpreter.endEntry(); + mTimeReadEndRecord += System.currentTimeMillis() - start; + } + return true; + } + + /** + * @return True when successful. False when reaching the end of line + * @throws IOException + * @throws VCardException + */ + protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { + String line; + do { + while (true) { + line = getLine(); + if (line == null) { + return false; + } else if (line.trim().length() > 0) { + break; + } + } + String[] strArray = line.split(":", 2); + int length = strArray.length; + + // Though vCard 2.1/3.0 specification does not allow lower cases, + // vCard file emitted by some external vCard expoter have such + // invalid Strings. + // So we allow it. + // e.g. BEGIN:vCard + if (length == 2 && strArray[0].trim().equalsIgnoreCase("BEGIN") + && strArray[1].trim().equalsIgnoreCase("VCARD")) { + return true; + } else if (!allowGarbage) { + if (mNestCount > 0) { + mPreviousLine = line; + return false; + } else { + throw new VCardException("Expected String \"BEGIN:VCARD\" did not come " + + "(Instead, \"" + line + "\" came)"); + } + } + } while (allowGarbage); + + throw new VCardException("Reached where must not be reached."); + } + + /** + * <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; + + if (mInterpreter != null) { + long start = System.currentTimeMillis(); + mInterpreter.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - start; + } + ended = parseItem(); + if (mInterpreter != null && !ended) { + long start = System.currentTimeMillis(); + mInterpreter.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - start; + } + + while (!ended) { + // follow VCARD ,it wont reach endProperty + if (mInterpreter != null) { + long start = System.currentTimeMillis(); + mInterpreter.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - start; + } + try { + ended = parseItem(); + } catch (VCardInvalidCommentLineException e) { + Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); + ended = false; + } + if (mInterpreter != null && !ended) { + long start = System.currentTimeMillis(); + mInterpreter.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - start; + } + } + } + + /* + * item = [groups "."] name [params] ":" value CRLF / [groups "."] "ADR" + * [params] ":" addressparts CRLF / [groups "."] "ORG" [params] ":" orgparts + * CRLF / [groups "."] "N" [params] ":" nameparts CRLF / [groups "."] + * "AGENT" [params] ":" vcard CRLF + */ + protected boolean parseItem() throws IOException, VCardException { + mCurrentEncoding = sDefaultEncoding; + + final String line = getNonEmptyLine(); + long start = System.currentTimeMillis(); + + String[] propertyNameAndValue = separateLineAndHandleGroup(line); + if (propertyNameAndValue == null) { + return true; + } + if (propertyNameAndValue.length != 2) { + throw new VCardInvalidLineException("Invalid line \"" + line + "\""); + } + String propertyName = propertyNameAndValue[0].toUpperCase(); + String propertyValue = propertyNameAndValue[1]; + + mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; + + if (propertyName.equals("ADR") || propertyName.equals("ORG") || propertyName.equals("N")) { + start = System.currentTimeMillis(); + handleMultiplePropertyValue(propertyName, propertyValue); + mTimeParseAdrOrgN += System.currentTimeMillis() - start; + return false; + } else if (propertyName.equals("AGENT")) { + handleAgent(propertyValue); + return false; + } else if (isValidPropertyName(propertyName)) { + if (propertyName.equals("BEGIN")) { + if (propertyValue.equals("VCARD")) { + throw new VCardNestedException("This vCard has nested vCard data in it."); + } else { + throw new VCardException("Unknown BEGIN type: " + propertyValue); + } + } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersionString())) { + throw new VCardVersionException("Incompatible version: " + propertyValue + " != " + + getVersionString()); + } + start = System.currentTimeMillis(); + handlePropertyValue(propertyName, propertyValue); + mTimeParsePropertyValues += System.currentTimeMillis() - start; + return false; + } + + throw new VCardException("Unknown property name: \"" + propertyName + "\""); + } + + // 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; + } + if (mInterpreter != 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 if (mInterpreter != null) { + 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; + } + if (mInterpreter != null) { + mInterpreter.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + nameIndex = i + 1; + state = STATE_PARAMS; // Start parameter parsing. + } + 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)); + } + if (mInterpreter != null) { + 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)); + } + if (mInterpreter != null) { + 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-")) { + if (mInterpreter != null) { + 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) { + if (mInterpreter != null) { + 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 + "\""); + } + } + if (mInterpreter != null) { + mInterpreter.propertyParamType("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) { + if (mInterpreter != null) { + 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); + if (mInterpreter != null) { + 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 String result = getBase64(propertyValue); + if (mInterpreter != null) { + ArrayList<String> arrayList = new ArrayList<String>(); + arrayList.add(result); + mInterpreter.propertyValues(arrayList); + } + } catch (OutOfMemoryError error) { + Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); + if (mInterpreter != null) { + 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())); + } + + final long start = System.currentTimeMillis(); + if (mInterpreter != null) { + 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); + } + + if (mInterpreter != null) { + mInterpreter.propertyValues(VCardUtils.constructListFromValue(propertyValue, + (getVersion() == VCardConfig.FLAG_V30))); + } + } + + /* + * 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"); + if (mReader instanceof CustomBufferedReader) { + Log.d(LOG_TAG, "Total readLine time: " + + ((CustomBufferedReader) mReader).getTotalmillisecond() + " ms"); + } + Log.d(LOG_TAG, "Time for handling the beggining of the record: " + mTimeReadStartRecord + + " ms"); + Log.d(LOG_TAG, "Time for handling the end of the record: " + mTimeReadEndRecord + " ms"); + Log.d(LOG_TAG, "Time for parsing line, and handling group: " + mTimeParseLineAndHandleGroup + + " ms"); + Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); + Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); + Log.d(LOG_TAG, "Time for handling normal property values: " + mTimeHandleMiscPropertyValue + + " ms"); + Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + mTimeHandleQuotedPrintable + " ms"); + Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); + } + + /** + * @return {@link VCardConfig#FLAG_V21} + */ + protected int getVersion() { + return VCardConfig.FLAG_V21; + } + + /** + * @return {@link VCardConfig#FLAG_V30} + */ + 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 sDefaultEncoding; + } + + + 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, mImportCharset); + if (VCardConfig.showPerformanceLog()) { + mReader = new CustomBufferedReader(tmpReader); + } else { + mReader = new BufferedReader(tmpReader); + } + + mInterpreter = interpreter; + + 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/vcard/java/com/android/vcard/VCardParserImpl_V30.java b/vcard/java/com/android/vcard/VCardParserImpl_V30.java new file mode 100644 index 0000000..61d0455 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardParserImpl_V30.java @@ -0,0 +1,317 @@ +/* + * 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, null); + } + + public VCardParserImpl_V30(int vcardType, String importCharset) { + super(vcardType, importCharset); + } + + @Override + protected int getVersion() { + return VCardConfig.FLAG_V30; + } + + @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) { + super.handleAnyParam(paramName, paramValue); + } + + @Override + protected void handleParamWithoutName(final String paramValue) throws VCardException { + super.handleParamWithoutName(paramValue); + } + + /* + * vCard 3.0 defines + * + * param = param-name "=" param-value *("," param-value) + * param-name = iana-token / x-name + * param-value = ptext / quoted-string + * quoted-string = DQUOTE QSAFE-CHAR DQUOTE + */ + @Override + protected void handleType(final String ptypevalues) { + String[] ptypeArray = ptypevalues.split(","); + mInterpreter.propertyParamType("TYPE"); + for (String value : ptypeArray) { + int length = value.length(); + if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) { + mInterpreter.propertyParamValue(value.substring(1, value.length() - 1)); + } else { + mInterpreter.propertyParamValue(value); + } + } + } + + @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/vcard/java/com/android/vcard/VCardParser_V21.java b/vcard/java/com/android/vcard/VCardParser_V21.java new file mode 100644 index 0000000..2a5e313 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardParser_V21.java @@ -0,0 +1,113 @@ +/* + * 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 VCardParser_V21(int parseType, String inputCharset) { + mVCardParserImpl = new VCardParserImpl_V21(parseType, null); + } + + public void parse(InputStream is, VCardInterpreter interepreter) + throws IOException, VCardException { + mVCardParserImpl.parse(is, interepreter); + } + + public void cancel() { + mVCardParserImpl.cancel(); + } +} diff --git a/vcard/java/com/android/vcard/VCardParser_V30.java b/vcard/java/com/android/vcard/VCardParser_V30.java new file mode 100644 index 0000000..179869b --- /dev/null +++ b/vcard/java/com/android/vcard/VCardParser_V30.java @@ -0,0 +1,91 @@ +/* + * 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 + + /** + * <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 VCardParser_V30(int vcardType, String importCharset) { + mVCardParserImpl = new VCardParserImpl_V30(vcardType, importCharset); + } + + public void parse(InputStream is, VCardInterpreter interepreter) + throws IOException, VCardException { + mVCardParserImpl.parse(is, interepreter); + } + + public void cancel() { + mVCardParserImpl.cancel(); + } +} diff --git a/vcard/java/com/android/vcard/VCardSourceDetector.java b/vcard/java/com/android/vcard/VCardSourceDetector.java new file mode 100644 index 0000000..e70d496 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardSourceDetector.java @@ -0,0 +1,172 @@ +/* + * 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 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 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"; + + + // TODO: Should replace this with types in VCardConfig + private 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. + + // Some mobile phones (like FOMA) tells us the charset of the data. + private boolean mNeedParseSpecifiedCharset; + private String mSpecifiedCharset; + + public void start() { + } + + public void end() { + } + + public void startEntry() { + } + + public void startProperty() { + mNeedParseSpecifiedCharset = false; + } + + public void endProperty() { + } + + public void endEntry() { + } + + public void propertyGroup(String group) { + } + + public void propertyName(String name) { + if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) { + mParseType = PARSE_TYPE_DOCOMO_TORELATE_NEST; + // Probably Shift_JIS is used, but we should double confirm. + mNeedParseSpecifiedCharset = 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 (mNeedParseSpecifiedCharset && 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. + */ + public int getEstimatedType() { + switch (mParseType) { + case PARSE_TYPE_DOCOMO_TORELATE_NEST: + return VCardConfig.VCARD_TYPE_DOCOMO | VCardConfig.FLAG_TORELATE_NEST; + case PARSE_TYPE_MOBILE_PHONE_JP: + return VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE; + case PARSE_TYPE_APPLE: + case PARSE_TYPE_WINDOWS_MOBILE_V65_JP: + default: + return VCardConfig.VCARD_TYPE_UNKNOWN; + } + } + + /** + * <p> + * Returns charset String guessed from the source's properties. + * This method must be called after parsing target file(s). + * </p> + * @return Charset String. Null is returned if guessing the source fails. + */ + public String getEstimatedCharset() { + if (TextUtils.isEmpty(mSpecifiedCharset)) { + return mSpecifiedCharset; + } + switch (mParseType) { + case PARSE_TYPE_WINDOWS_MOBILE_V65_JP: + case PARSE_TYPE_DOCOMO_TORELATE_NEST: + case PARSE_TYPE_MOBILE_PHONE_JP: + return "SHIFT_JIS"; + case PARSE_TYPE_APPLE: + return "UTF-8"; + default: + return null; + } + } +} diff --git a/vcard/java/com/android/vcard/VCardUtils.java b/vcard/java/com/android/vcard/VCardUtils.java new file mode 100644 index 0000000..fb0c2e7 --- /dev/null +++ b/vcard/java/com/android/vcard/VCardUtils.java @@ -0,0 +1,658 @@ +/* + * 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 org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.net.QuotedPrintableCodec; + +import android.content.ContentProviderOperation; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Utilities for VCard handling codes. + */ +public class VCardUtils { + private static final String LOG_TAG = "VCardUtils"; + + // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is + // converted to two parameter Strings. These only contain some minor fields valid in both + // vCard and current (as of 2009-08-07) Contacts structure. + private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS; + private static final Set<String> sPhoneTypesUnknownToContactsSet; + private static final Map<String, Integer> sKnownPhoneTypeMap_StoI; + private static final Map<Integer, String> sKnownImPropNameMap_ItoS; + private static final Set<String> sMobilePhoneLabelSet; + + static { + sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>(); + sKnownPhoneTypeMap_StoI = new HashMap<String, Integer>(); + + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN); + + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE); + + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, + Phone.TYPE_CALLBACK); + sKnownPhoneTypeMap_StoI.put( + VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, + Phone.TYPE_TTY_TDD); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT, + Phone.TYPE_ASSISTANT); + + sPhoneTypesUnknownToContactsSet = new HashSet<String>(); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MSG); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO); + + sKnownImPropNameMap_ItoS = new HashMap<Integer, String>(); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, + VCardConstants.PROPERTY_X_GOOGLE_TALK); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING); + + // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone) + // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone) + // \u30B1\u30A4\u30BF\u30A4 = Full-width Katakana "Keitai" (mobile phone) + // \uFF79\uFF72\uFF80\uFF72 = Half-width Katakana "Keitai" (mobile phone) + sMobilePhoneLabelSet = new HashSet<String>(Arrays.asList( + "MOBILE", "\u643A\u5E2F\u96FB\u8A71", "\u643A\u5E2F", "\u30B1\u30A4\u30BF\u30A4", + "\uFF79\uFF72\uFF80\uFF72")); + } + + public static String getPhoneTypeString(Integer type) { + return sKnownPhoneTypesMap_ItoS.get(type); + } + + /** + * Returns Interger when the given types can be parsed as known type. Returns String object + * when not, which should be set to label. + */ + public static Object getPhoneTypeFromStrings(Collection<String> types, + String number) { + if (number == null) { + number = ""; + } + int type = -1; + String label = null; + boolean isFax = false; + boolean hasPref = false; + + if (types != null) { + for (String typeString : types) { + if (typeString == null) { + continue; + } + typeString = typeString.toUpperCase(); + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { + hasPref = true; + } else if (typeString.equals(VCardConstants.PARAM_TYPE_FAX)) { + isFax = true; + } else { + if (typeString.startsWith("X-") && type < 0) { + typeString = typeString.substring(2); + } + if (typeString.length() == 0) { + continue; + } + final Integer tmp = sKnownPhoneTypeMap_StoI.get(typeString); + if (tmp != null) { + final int typeCandidate = tmp; + // TYPE_PAGER is prefered when the number contains @ surronded by + // a pager number and a domain name. + // e.g. + // o 1111@domain.com + // x @domain.com + // x 1111@ + final int indexOfAt = number.indexOf("@"); + if ((typeCandidate == Phone.TYPE_PAGER + && 0 < indexOfAt && indexOfAt < number.length() - 1) + || type < 0 + || type == Phone.TYPE_CUSTOM) { + type = tmp; + } + } else if (type < 0) { + type = Phone.TYPE_CUSTOM; + label = typeString; + } + } + } + } + if (type < 0) { + if (hasPref) { + type = Phone.TYPE_MAIN; + } else { + // default to TYPE_HOME + type = Phone.TYPE_HOME; + } + } + if (isFax) { + if (type == Phone.TYPE_HOME) { + type = Phone.TYPE_FAX_HOME; + } else if (type == Phone.TYPE_WORK) { + type = Phone.TYPE_FAX_WORK; + } else if (type == Phone.TYPE_OTHER) { + type = Phone.TYPE_OTHER_FAX; + } + } + if (type == Phone.TYPE_CUSTOM) { + return label; + } else { + return type; + } + } + + @SuppressWarnings("deprecation") + public static boolean isMobilePhoneLabel(final String label) { + // For backward compatibility. + // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. + // To support mobile type at that time, this custom label had been used. + return ("_AUTO_CELL".equals(label) || sMobilePhoneLabelSet.contains(label)); + } + + public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) { + return sPhoneTypesUnknownToContactsSet.contains(label); + } + + public static String getPropertyNameForIm(final int protocol) { + return sKnownImPropNameMap_ItoS.get(protocol); + } + + public static String[] sortNameElements(final int vcardType, + final String familyName, final String middleName, final String givenName) { + final String[] list = new String[3]; + final int nameOrderType = VCardConfig.getNameOrderType(vcardType); + switch (nameOrderType) { + case VCardConfig.NAME_ORDER_JAPANESE: { + if (containsOnlyPrintableAscii(familyName) && + containsOnlyPrintableAscii(givenName)) { + list[0] = givenName; + list[1] = middleName; + list[2] = familyName; + } else { + list[0] = familyName; + list[1] = middleName; + list[2] = givenName; + } + break; + } + case VCardConfig.NAME_ORDER_EUROPE: { + list[0] = middleName; + list[1] = givenName; + list[2] = familyName; + break; + } + default: { + list[0] = givenName; + list[1] = middleName; + list[2] = familyName; + break; + } + } + return list; + } + + public static int getPhoneNumberFormat(final int vcardType) { + if (VCardConfig.isJapaneseDevice(vcardType)) { + return PhoneNumberUtils.FORMAT_JAPAN; + } else { + return PhoneNumberUtils.FORMAT_NANP; + } + } + + /** + * <p> + * Inserts postal data into the builder object. + * </p> + * <p> + * Note that the data structure of ContactsContract is different from that defined in vCard. + * So some conversion may be performed in this method. + * </p> + */ + public static void insertStructuredPostalDataUsingContactsStruct(int vcardType, + final ContentProviderOperation.Builder builder, + final VCardEntry.PostalData postalData) { + builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE); + + builder.withValue(StructuredPostal.TYPE, postalData.type); + if (postalData.type == StructuredPostal.TYPE_CUSTOM) { + builder.withValue(StructuredPostal.LABEL, postalData.label); + } + + final String streetString; + if (TextUtils.isEmpty(postalData.street)) { + if (TextUtils.isEmpty(postalData.extendedAddress)) { + streetString = null; + } else { + streetString = postalData.extendedAddress; + } + } else { + if (TextUtils.isEmpty(postalData.extendedAddress)) { + streetString = postalData.street; + } else { + streetString = postalData.street + " " + postalData.extendedAddress; + } + } + builder.withValue(StructuredPostal.POBOX, postalData.pobox); + builder.withValue(StructuredPostal.STREET, streetString); + builder.withValue(StructuredPostal.CITY, postalData.localty); + builder.withValue(StructuredPostal.REGION, postalData.region); + builder.withValue(StructuredPostal.POSTCODE, postalData.postalCode); + builder.withValue(StructuredPostal.COUNTRY, postalData.country); + + builder.withValue(StructuredPostal.FORMATTED_ADDRESS, + postalData.getFormattedAddress(vcardType)); + if (postalData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); + } + } + + public static String constructNameFromElements(final int vcardType, + final String familyName, final String middleName, final String givenName) { + return constructNameFromElements(vcardType, familyName, middleName, givenName, + null, null); + } + + public static String constructNameFromElements(final int vcardType, + final String familyName, final String middleName, final String givenName, + final String prefix, final String suffix) { + final StringBuilder builder = new StringBuilder(); + final String[] nameList = sortNameElements(vcardType, familyName, middleName, givenName); + boolean first = true; + if (!TextUtils.isEmpty(prefix)) { + first = false; + builder.append(prefix); + } + for (final String namePart : nameList) { + if (!TextUtils.isEmpty(namePart)) { + if (first) { + first = false; + } else { + builder.append(' '); + } + builder.append(namePart); + } + } + if (!TextUtils.isEmpty(suffix)) { + if (!first) { + builder.append(' '); + } + builder.append(suffix); + } + return builder.toString(); + } + + public static List<String> constructListFromValue(final String value, + final boolean isV30) { + final List<String> list = new ArrayList<String>(); + StringBuilder builder = new StringBuilder(); + int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (ch == '\\' && i < length - 1) { + char nextCh = value.charAt(i + 1); + final String unescapedString = + (isV30 ? VCardParserImpl_V30.unescapeCharacter(nextCh) : + VCardParserImpl_V21.unescapeCharacter(nextCh)); + if (unescapedString != null) { + builder.append(unescapedString); + i++; + } else { + builder.append(ch); + } + } else if (ch == ';') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else { + builder.append(ch); + } + } + list.add(builder.toString()); + return list; + } + + public static boolean containsOnlyPrintableAscii(final String...values) { + if (values == null) { + return true; + } + return containsOnlyPrintableAscii(Arrays.asList(values)); + } + + public static boolean containsOnlyPrintableAscii(final Collection<String> values) { + if (values == null) { + return true; + } + for (final String value : values) { + if (TextUtils.isEmpty(value)) { + continue; + } + if (!TextUtils.isPrintableAsciiOnly(value)) { + return false; + } + } + return true; + } + + /** + * <p> + * This is useful when checking the string should be encoded into quoted-printable + * or not, which is required by vCard 2.1. + * </p> + * <p> + * See the definition of "7bit" in vCard 2.1 spec for more information. + * </p> + */ + public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) { + if (values == null) { + return true; + } + return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values)); + } + + public static boolean containsOnlyNonCrLfPrintableAscii(final Collection<String> values) { + if (values == null) { + return true; + } + final int asciiFirst = 0x20; + final int asciiLast = 0x7E; // included + for (final String value : values) { + if (TextUtils.isEmpty(value)) { + continue; + } + final int length = value.length(); + for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { + final int c = value.codePointAt(i); + if (!(asciiFirst <= c && c <= asciiLast)) { + return false; + } + } + } + return true; + } + + private static final Set<Character> sUnAcceptableAsciiInV21WordSet = + new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' ')); + + /** + * <p> + * This is useful since vCard 3.0 often requires the ("X-") properties and groups + * should contain only alphabets, digits, and hyphen. + * </p> + * <p> + * Note: It is already known some devices (wrongly) outputs properties with characters + * which should not be in the field. One example is "X-GOOGLE TALK". We accept + * such kind of input but must never output it unless the target is very specific + * to the device which is able to parse the malformed input. + * </p> + */ + public static boolean containsOnlyAlphaDigitHyphen(final String...values) { + if (values == null) { + return true; + } + return containsOnlyAlphaDigitHyphen(Arrays.asList(values)); + } + + public static boolean containsOnlyAlphaDigitHyphen(final Collection<String> values) { + if (values == null) { + return true; + } + final int upperAlphabetFirst = 0x41; // A + final int upperAlphabetAfterLast = 0x5b; // [ + final int lowerAlphabetFirst = 0x61; // a + final int lowerAlphabetAfterLast = 0x7b; // { + final int digitFirst = 0x30; // 0 + final int digitAfterLast = 0x3A; // : + final int hyphen = '-'; + for (final String str : values) { + if (TextUtils.isEmpty(str)) { + continue; + } + final int length = str.length(); + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + int codepoint = str.codePointAt(i); + if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetAfterLast) || + (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetAfterLast) || + (digitFirst <= codepoint && codepoint < digitAfterLast) || + (codepoint == hyphen))) { + return false; + } + } + } + return true; + } + + /** + * <p> + * Returns true when the given String is categorized as "word" specified in vCard spec 2.1. + * </p> + * <p> + * vCard 2.1 specifies:<br /> + * word = <any printable 7bit us-ascii except []=:., > + * </p> + */ + public static boolean isV21Word(final String value) { + if (TextUtils.isEmpty(value)) { + return true; + } + final int asciiFirst = 0x20; + final int asciiLast = 0x7E; // included + final int length = value.length(); + for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { + final int c = value.codePointAt(i); + if (!(asciiFirst <= c && c <= asciiLast) || + sUnAcceptableAsciiInV21WordSet.contains((char)c)) { + return false; + } + } + return true; + } + + public static String toHalfWidthString(final String orgString) { + if (TextUtils.isEmpty(orgString)) { + return null; + } + final StringBuilder builder = new StringBuilder(); + final int length = orgString.length(); + for (int i = 0; i < length; i = orgString.offsetByCodePoints(i, 1)) { + // All Japanese character is able to be expressed by char. + // Do not need to use String#codepPointAt(). + final char ch = orgString.charAt(i); + final String halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch); + if (halfWidthText != null) { + builder.append(halfWidthText); + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + /** + * Guesses the format of input image. Currently just the first few bytes are used. + * The type "GIF", "PNG", or "JPEG" is returned when possible. Returns null when + * the guess failed. + * @param input Image as byte array. + * @return The image type or null when the type cannot be determined. + */ + public static String guessImageType(final byte[] input) { + if (input == null) { + return null; + } + if (input.length >= 3 && input[0] == 'G' && input[1] == 'I' && input[2] == 'F') { + return "GIF"; + } else if (input.length >= 4 && input[0] == (byte) 0x89 + && input[1] == 'P' && input[2] == 'N' && input[3] == 'G') { + // Note: vCard 2.1 officially does not support PNG, but we may have it and + // using X- word like "X-PNG" may not let importers know it is PNG. + // So we use the String "PNG" as is... + return "PNG"; + } else if (input.length >= 2 && input[0] == (byte) 0xff + && input[1] == (byte) 0xd8) { + return "JPEG"; + } else { + return null; + } + } + + /** + * @return True when all the given values are null or empty Strings. + */ + public static boolean areAllEmpty(final String...values) { + if (values == null) { + return true; + } + + for (final String value : values) { + if (!TextUtils.isEmpty(value)) { + return false; + } + } + return true; + } + + //// The methods bellow may be used by unit test. + + /** + * @hide + */ + public static String parseQuotedPrintable(String value, boolean strictLineBreaking, + String sourceCharset, String targetCharset) { + // "= " -> " ", "=\t" -> "\t". + // Previous code had done this replacement. Keep on the safe side. + final String quotedPrintable; + { + final StringBuilder builder = new StringBuilder(); + final int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (ch == '=' && i < length - 1) { + char nextCh = value.charAt(i + 1); + if (nextCh == ' ' || nextCh == '\t') { + builder.append(nextCh); + i++; + continue; + } + } + builder.append(ch); + } + quotedPrintable = builder.toString(); + } + + String[] lines; + if (strictLineBreaking) { + lines = quotedPrintable.split("\r\n"); + } else { + StringBuilder builder = new StringBuilder(); + final int length = quotedPrintable.length(); + ArrayList<String> list = new ArrayList<String>(); + for (int i = 0; i < length; i++) { + char ch = quotedPrintable.charAt(i); + if (ch == '\n') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else if (ch == '\r') { + list.add(builder.toString()); + builder = new StringBuilder(); + if (i < length - 1) { + char nextCh = quotedPrintable.charAt(i + 1); + if (nextCh == '\n') { + i++; + } + } + } else { + builder.append(ch); + } + } + final String lastLine = builder.toString(); + if (lastLine.length() > 0) { + list.add(lastLine); + } + lines = list.toArray(new String[0]); + } + + final StringBuilder builder = new StringBuilder(); + for (String line : lines) { + if (line.endsWith("=")) { + line = line.substring(0, line.length() - 1); + } + builder.append(line); + } + + final String rawString = builder.toString(); + if (TextUtils.isEmpty(rawString)) { + Log.w(LOG_TAG, "Given raw string is empty."); + } + + byte[] rawBytes = null; + try { + rawBytes = rawString.getBytes(sourceCharset); + } catch (UnsupportedEncodingException e) { + Log.w(LOG_TAG, "Failed to decode: " + sourceCharset); + rawBytes = rawString.getBytes(); + } + + byte[] decodedBytes = null; + try { + decodedBytes = QuotedPrintableCodec.decodeQuotedPrintable(rawBytes); + } catch (DecoderException e) { + Log.e(LOG_TAG, "DecoderException is thrown."); + decodedBytes = rawBytes; + } + + try { + return new String(decodedBytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return new String(decodedBytes); + } + } + + private VCardUtils() { + } +} diff --git a/vcard/java/com/android/vcard/exception/VCardAgentNotSupportedException.java b/vcard/java/com/android/vcard/exception/VCardAgentNotSupportedException.java new file mode 100644 index 0000000..c408716 --- /dev/null +++ b/vcard/java/com/android/vcard/exception/VCardAgentNotSupportedException.java @@ -0,0 +1,27 @@ +/* + * 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.exception; + +public class VCardAgentNotSupportedException extends VCardNotSupportedException { + public VCardAgentNotSupportedException() { + super(); + } + + public VCardAgentNotSupportedException(String message) { + super(message); + } + +}
\ No newline at end of file diff --git a/vcard/java/com/android/vcard/exception/VCardException.java b/vcard/java/com/android/vcard/exception/VCardException.java new file mode 100644 index 0000000..3ad7fd3 --- /dev/null +++ b/vcard/java/com/android/vcard/exception/VCardException.java @@ -0,0 +1,35 @@ +/* + * 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.exception; + +public class VCardException extends java.lang.Exception { + /** + * Constructs a VCardException object + */ + public VCardException() { + super(); + } + + /** + * Constructs a VCardException object + * + * @param message the error message + */ + public VCardException(String message) { + super(message); + } + +} diff --git a/vcard/java/com/android/vcard/exception/VCardInvalidCommentLineException.java b/vcard/java/com/android/vcard/exception/VCardInvalidCommentLineException.java new file mode 100644 index 0000000..342769e --- /dev/null +++ b/vcard/java/com/android/vcard/exception/VCardInvalidCommentLineException.java @@ -0,0 +1,32 @@ +/* + * 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.exception; + +/** + * Thrown when the vCard has some line starting with '#'. In the specification, + * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit + * such lines. + */ +public class VCardInvalidCommentLineException extends VCardInvalidLineException { + public VCardInvalidCommentLineException() { + super(); + } + + public VCardInvalidCommentLineException(final String message) { + super(message); + } +} diff --git a/vcard/java/com/android/vcard/exception/VCardInvalidLineException.java b/vcard/java/com/android/vcard/exception/VCardInvalidLineException.java new file mode 100644 index 0000000..5c2250f --- /dev/null +++ b/vcard/java/com/android/vcard/exception/VCardInvalidLineException.java @@ -0,0 +1,31 @@ +/* + * 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.exception; + +/** + * Thrown when the vCard has some line starting with '#'. In the specification, + * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit + * such lines. + */ +public class VCardInvalidLineException extends VCardException { + public VCardInvalidLineException() { + super(); + } + + public VCardInvalidLineException(final String message) { + super(message); + } +} diff --git a/vcard/java/com/android/vcard/exception/VCardNestedException.java b/vcard/java/com/android/vcard/exception/VCardNestedException.java new file mode 100644 index 0000000..2b9b1ac --- /dev/null +++ b/vcard/java/com/android/vcard/exception/VCardNestedException.java @@ -0,0 +1,29 @@ +/* + * 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.exception; + +/** + * VCardException thrown when VCard is nested without VCardParser's being notified. + */ +public class VCardNestedException extends VCardNotSupportedException { + public VCardNestedException() { + super(); + } + public VCardNestedException(String message) { + super(message); + } +} diff --git a/vcard/java/com/android/vcard/exception/VCardNotSupportedException.java b/vcard/java/com/android/vcard/exception/VCardNotSupportedException.java new file mode 100644 index 0000000..61ff752 --- /dev/null +++ b/vcard/java/com/android/vcard/exception/VCardNotSupportedException.java @@ -0,0 +1,33 @@ +/* + * 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.exception; + +/** + * The exception which tells that the input VCard is probably valid from the view of + * specification but not supported in the current framework for now. + * + * This is a kind of a good news from the view of development. + * It may be good to ask users to send a report with the VCard example + * for the future development. + */ +public class VCardNotSupportedException extends VCardException { + public VCardNotSupportedException() { + super(); + } + public VCardNotSupportedException(String message) { + super(message); + } +}
\ No newline at end of file diff --git a/vcard/java/com/android/vcard/exception/VCardVersionException.java b/vcard/java/com/android/vcard/exception/VCardVersionException.java new file mode 100644 index 0000000..047c580 --- /dev/null +++ b/vcard/java/com/android/vcard/exception/VCardVersionException.java @@ -0,0 +1,28 @@ +/* + * 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.exception; + +/** + * VCardException used only when the version of the vCard is different. + */ +public class VCardVersionException extends VCardException { + public VCardVersionException() { + super(); + } + public VCardVersionException(String message) { + super(message); + } +} |