diff --git a/vcard/Android.mk b/vcard/Android.mk new file mode 100644 index 000000000..2bc17aa8c --- /dev/null +++ b/vcard/Android.mk @@ -0,0 +1,28 @@ +# 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE := com.android.vcard +LOCAL_SRC_FILES := $(call all-java-files-under, java) + +# Use google-common instead of android-common for using hidden code in telephony library. +# Use ext for using Quoted-Printable codec. +LOCAL_JAVA_LIBRARIES := google-common ext + +include $(BUILD_STATIC_JAVA_LIBRARY) + +# Build the test package. +include $(call all-makefiles-under, $(LOCAL_PATH)) diff --git a/vcard/java/com/android/vcard/JapaneseUtils.java b/vcard/java/com/android/vcard/JapaneseUtils.java new file mode 100644 index 000000000..5b4494469 --- /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 sHalfWidthMap = + new HashMap(); + + 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 000000000..6ef9adad9 --- /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; + +/** + *

+ * The class which lets users create their own vCard String. Typical usage is as follows: + *

+ *
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();
+ */ +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 sAllowedAndroidPropertySet = + Collections.unmodifiableSet(new HashSet(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 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 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 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 contentValuesList) { + boolean phoneLineExists = false; + if (contentValuesList != null) { + Set phoneSet = new HashSet(); + 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 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; + } + + /** + *

+ * 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. + *

+ *

+ * 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)" + *

+ *

+ * 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. + *

+ *

+ * 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)". + *

+ *

+ * Do not call this method when trimming is inappropriate for its receivers. + *

+ */ + private List splitAndTrimPhoneNumbers(final String phoneNumber) { + final List phoneList = new ArrayList(); + + 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 contentValuesList) { + boolean emailAddressExists = false; + if (contentValuesList != null) { + final Set addressSet = new HashSet(); + 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 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 sPostalTypePriorityMap; + + static { + sPostalTypePriorityMap = new HashMap(); + 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 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 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 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 parameterList = new ArrayList(); + 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 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 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 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 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 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 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 parameterList = new ArrayList(); + 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 parameterList = new ArrayList(); + 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 parameterList = new ArrayList(); + 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 rawValueList = new ArrayList(); + 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 rawValueList) { + appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList); + } + + public void appendLineWithCharsetAndQPDetection(final String propertyName, + final List 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 parameterList, final List 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 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 parameterList, + final String rawValue) { + appendLine(propertyName, parameterList, rawValue, false, false); + } + + public void appendLine(final String propertyName, final List 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 rawValueList, + final boolean needCharset, boolean needQuotedPrintable) { + appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable); + } + + public void appendLine(final String propertyName, final List parameterList, + final List 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 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 000000000..703895575 --- /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; + +/** + *

+ * The class for composing vCard from Contacts information. + *

+ *

+ * Usually, this class should be used like this. + *

+ *
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();
+ *     }
+ * }
+ *

+ * Users have to manually take care of memory efficiency. Even one vCard may contain + * image of non-trivial size for mobile devices. + *

+ *

+ * {@link VCardBuilder} is used to build each vCard. + *

+ */ +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 sImMap; + + static { + sImMap = new HashMap(); + 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(); + } + + /** + *

+ * An useful handler for emitting vCard String to an OutputStream object one by one. + *

+ *

+ * The input OutputStream object is closed() on {@link #onTerminate()}. + * Must not close the stream outside this class. + *

+ */ + 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 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(); + + 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 finishedList = new ArrayList( + 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 finishedList = new ArrayList( + 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> contentValuesListMap = + new HashMap>(); + // 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 contentValuesList = + contentValuesListMap.get(key); + if (contentValuesList == null) { + contentValuesList = new ArrayList(); + 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> 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 000000000..fc95922d9 --- /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; + + /** + *

+ * The charset used during import. + *

+ *

+ * 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. + *

+ */ + 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 + + /** + *

+ * 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. + *

+ *

+ * 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. + *

+ *

+ * vCard parser code automatically parses the field emitted even when this flag is off. + *

+ */ + private static final int FLAG_USE_ANDROID_PROPERTY = 0x80000000; + + /** + *

+ * 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. + *

+ *

+ * 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. + *

+ *

+ * 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. + *

+ */ + private static final int FLAG_USE_DEFACT_PROPERTY = 0x40000000; + + /** + *

+ * 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. + *

+ */ + private static final int FLAG_DOCOMO = 0x20000000; + + /** + *

+ * 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). + *

+ *

+ * 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. + *

+ *

+ * 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. + *

+ * We are afraid of the case where some vCard importer also forget handling QP presuming QP is + * not used in such fields. + *

+ *

+ * This flag is useful when some target importer you are going to focus on does not accept + * such properties with Quoted-Printable encoding. + *

+ *

+ * Again, we should not use this flag at all for complying vCard 2.1 spec. + *

+ *

+ * In vCard 3.0, Quoted-Printable is explicitly "prohibitted", so we don't need to care this + * kind of problem (hopefully). + *

+ * @hide + */ + public static final int FLAG_REFRAIN_QP_TO_NAME_PROPERTIES = 0x10000000; + + /** + *

+ * 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. + *

+ *

+ * 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. + *

+ */ + public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x08000000; + + /** + *

+ * 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. + *

+ *

+ * Detail: + * How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0. + *

+ *

+ * e.g. + *

+ *
    + *
  1. Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..."
  2. + *
  3. Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..."
  4. + *
  5. Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..."
  6. + *
+ *

+ * If you are targeting to the importer which cannot accept TYPE params without "TYPE=" + * strings (which should be rare though), please use this flag. + *

+ *

+ * Example usage: + *

int type = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM);
+ *

+ */ + public static final int FLAG_APPEND_TYPE_PARAM = 0x04000000; + + /** + *

+ * The flag indicating the vCard composer does touch nothing toward phone number Strings + * but leave it as is. + *

+ *

+ * 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. + *

+ *

+ * 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. + *

+ */ + public static final int FLAG_REFRAIN_PHONE_NUMBER_FORMATTING = 0x02000000; + + /** + *

+ * For importer only. Ignored in exporter. + *

+ *

+ * The flag indicating the parser should handle a nested vCard, in which vCard clause starts + * in another vCard clause. Here's a typical example. + *

+ *
BEGIN:VCARD
+     * BEGIN:VCARD
+     * VERSION:2.1
+     * ...
+     * END:VCARD
+     * END:VCARD
+ *

+ * 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. + *

+ *

+ * This flag forces a vCard parser to torelate such a nest and understand its content. + *

+ */ + public static final int FLAG_TORELATE_NEST = 0x01000000; + + //// The followings are VCard types available from importer/exporter. //// + + /** + *

+ * The type indicating nothing. Used by {@link VCardSourceDetector} when it + * was not able to guess the exact vCard type. + *

+ */ + public static final int VCARD_TYPE_UNKNOWN = 0; + + /** + *

+ * Generic vCard format with the vCard 2.1. When composing a vCard entry, + * the US convension will be used toward formatting some values. + *

+ *

+ * 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. + *

+ *

+ * 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. + *

+ *

+ * If you want to use alternative charset, you should notify the charset to the other + * compontent to be used. + *

+ */ + 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"; + + /** + *

+ * General vCard format with the version 3.0. Uses UTF-8 for the charset. + *

+ *

+ * Not fully ready yet. Use with caution when you use this. + *

+ */ + 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"; + + /** + *

+ * 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") + *

+ */ + 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"; + + /** + *

+ * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8. + *

+ *

+ * Not ready yet. Use with caution when you use this. + *

+ */ + 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"; + + /** + *

+ * The vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. + *

+ *

+ * Not ready yet. Use with caution when you use this. + *

+ */ + 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"; + + /** + *

+ * The vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. + *

+ *

+ * Not ready yet. Use with caution when you use this. + *

+ */ + 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"; + + /** + *

+ * 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. + *

+ * @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"; + + /** + *

+ * The vCard format used in DoCoMo, which is one of Japanese mobile phone careers. + *

+ *

+ * 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. + *

+ * @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 sVCardTypeMap; + private static final Set sJapaneseMobileTypeSet; + + static { + sVCardTypeMap = new HashMap(); + 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(); + 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 000000000..862c9edcb --- /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 000000000..624407a35 --- /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 sImMap = new HashMap(); + + 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 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> mParameterMap = + new HashMap>(); + private List mPropertyValueList = new ArrayList(); + private byte[] mPropertyBytes; + + public void setPropertyName(final String propertyName) { + mPropertyName = propertyName; + } + + public void addParameter(final String paramName, final String paramValue) { + Collection values; + if (!mParameterMap.containsKey(paramName)) { + if (paramName.equals("TYPE")) { + values = new HashSet(); + } else { + values = new ArrayList(); + } + 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 getParameters(String type) { + return mParameterMap.get(type); + } + + public final List 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 mNickNameList; + + private String mDisplayName; + + private String mBirthday; + + private List mNoteList; + private List mPhoneList; + private List mEmailList; + private List mPostalList; + private List mOrganizationList; + private List mImList; + private List mPhotoList; + private List mWebsiteList; + private List> 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(); + } + 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(); + } + mNickNameList.add(nickName); + } + + private void addEmail(int type, String data, String label, boolean isPrimary){ + if (mEmailList == null) { + mEmailList = new ArrayList(); + } + mEmailList.add(new EmailData(type, data, label, isPrimary)); + } + + private void addPostal(int type, List propValueList, String label, boolean isPrimary){ + if (mPostalList == null) { + mPostalList = new ArrayList(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(); + } + mOrganizationList.add(new OrganizationData(type, companyName, + departmentName, titleName, isPrimary)); + } + + private static final List sEmptyList = + Collections.unmodifiableList(new ArrayList(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 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(); + } + mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary)); + } + + private void addNote(final String note) { + if (mNoteList == null) { + mNoteList = new ArrayList(1); + } + mNoteList.add(note); + } + + private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) { + if (mPhotoList == null) { + mPhotoList = new ArrayList(1); + } + final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary); + mPhotoList.add(photoData); + } + + @SuppressWarnings("fallthrough") + private void handleNProperty(List 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 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> paramMap = property.mParameterMap; + final List 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 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 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 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 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 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 paramMapValue = paramMap.get("VALUE"); + if (paramMapValue != null && paramMapValue.contains("URL")) { + // Currently we do not have appropriate example for testing this case. + } else { + final Collection 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 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 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 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(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 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 customPropertyList) { + if (mAndroidCustomPropertyList == null) { + mAndroidCustomPropertyList = new ArrayList>(); + } + 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 operationList = + new ArrayList(); + // 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 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 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 getNickNameList() { + return mNickNameList; + } + + public String getBirthday() { + return mBirthday; + } + + public final List getNotes() { + return mNoteList; + } + + public final List getPhoneList() { + return mPhoneList; + } + + public final List getEmailList() { + return mEmailList; + } + + public final List getPostalList() { + return mPostalList; + } + + public final List getOrganizationList() { + return mOrganizationList; + } + + public final List getImList() { + return mImList; + } + + public final List getPhotoList() { + return mPhotoList; + } + + public final List 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 000000000..7bd314eb3 --- /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; + +/** + *

+ * {@link VCardEntryHandler} implementation which commits the entry to ContentResolver. + *

+ *

+ * Note:
+ * 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. + *

+ */ +public class VCardEntryCommitter implements VCardEntryHandler { + public static String LOG_TAG = "VCardEntryComitter"; + + private final ContentResolver mContentResolver; + private long mTimeToCommit; + private ArrayList mCreatedUris = new ArrayList(); + + 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 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 000000000..2679e238a --- /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; + +/** + *

+ * The {@link VCardInterpreter} implementation which enables {@link VCardEntryHandler} objects + * to easily handle each vCard entry. + *

+ *

+ * 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. + *

+ *

+ * 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. + *

+ */ +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 mEntryHandlers = new ArrayList(); + + 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 values) { + if (values == null || values.isEmpty()) { + return; + } + + final Collection charsetCollection = mCurrentProperty.getParameters("CHARSET"); + final Collection 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 000000000..7bfe9773f --- /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 values) { + } +} diff --git a/vcard/java/com/android/vcard/VCardEntryHandler.java b/vcard/java/com/android/vcard/VCardEntryHandler.java new file mode 100644 index 000000000..ef35a20a2 --- /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; + +/** + *

+ * The interface called by {@link VCardEntryConstructor}. + *

+ *

+ * 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}. + *

+ */ +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 000000000..2d987644f --- /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; + +/** + *

+ * The interface which should be implemented by the classes which have to analyze each + * vCard entry minutely. + *

+ *

+ * Here, there are several terms specific to vCard (and this library). + *

+ *

+ * The term "entry" is one vCard representation in the input, which should start with "BEGIN:VCARD" + * and end with "END:VCARD". + *

+ *

+ * The term "property" is one line in vCard entry, which consists of "group", "property name", + * "parameter(param) names and values", and "property values". + *

+ *

+ * e.g. group1.propName;paramName1=paramValue1;paramName2=paramValue2;propertyValue1;propertyValue2... + *

+ */ +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 values); +} diff --git a/vcard/java/com/android/vcard/VCardInterpreterCollection.java b/vcard/java/com/android/vcard/VCardInterpreterCollection.java new file mode 100644 index 000000000..4a40d9312 --- /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 mInterpreterCollection; + + public VCardInterpreterCollection(Collection interpreterCollection) { + mInterpreterCollection = interpreterCollection; + } + + public Collection 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 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 000000000..b7b8291dd --- /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 { + /** + *

+ * Parses the given stream and send the vCard data into VCardBuilderBase object. + *

. + *

+ * 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. + *

+ *

+ * We recommend you use {@link VCardSourceDetector} and detect which kind of source the + * vCard comes from and explicitly specify a charset using the result. + *

+ * + * @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; + + /** + *

+ * Cancel parsing vCard. Useful when you want to stop the parse in the other threads. + *

+ *

+ * Actual cancel is done after parsing the current vcard. + *

+ */ + 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 000000000..00ae6c91d --- /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; + +/** + *

+ * Basic implementation achieving vCard parsing. Based on vCard 2.1, + *

+ * @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; + + /** + *

+ * The encoding type for deconding byte streams. This member variable is + * reset to a default encoding every time when a new item comes. + *

+ *

+ * "Encoding" in vCard is different from "Charset". It is mainly used for + * addresses, notes, images. "7BIT", "8BIT", "BASE64", and + * "QUOTED-PRINTABLE" are known examples. + *

+ */ + protected String mCurrentEncoding; + + /** + *

+ * The reader object to be used internally. + *

+ *

+ * Developers should not directly read a line from this object. Use + * getLine() unless there some reason. + *

+ */ + protected BufferedReader mReader; + + /** + *

+ * Set for storing unkonwn TYPE attributes, which is not acceptable in vCard + * specification, but happens to be seen in real world vCard. + *

+ */ + protected final Set mUnknownTypeSet = new HashSet(); + + /** + *

+ * Set for storing unkonwn VALUE attributes, which is not acceptable in + * vCard specification, but happens to be seen in real world vCard. + *

+ */ + protected final Set mUnknownValueSet = new HashSet(); + + + // 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); + } + + /** + *

+ * Parses the file at the given position. + *

+ */ + //
vcard_file = [wsls] vcard [wsls]
+ 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."); + } + + /** + *

+ * The arguments useCache and allowGarbase are usually true and false + * accordingly when this function is called outside this function itself. + *

+ * + * @param useCache When true, line is obtained from mPreviousline. + * Otherwise, getLine() is used. + * @param allowGarbage When true, ignore non "END:VCARD" line. + * @throws IOException + * @throws VCardException + */ + protected void readEndVCard(boolean useCache, boolean allowGarbage) 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 + "\""); + } + } + + /** + *

+ * 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. + *

+ */ + 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 v = new ArrayList(); + 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 arrayList = new ArrayList(); + 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 v = new ArrayList(); + v.add(maybeUnescapeText(propertyValue)); + mInterpreter.propertyValues(v); + } + mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; + } + } + + /** + *

+ * Parses and returns Quoted-Printable. + *

+ * + * @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(); + } + + /** + *

+ * Mainly for "ADR", "ORG", and "N" + *

+ */ + /* + * 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 getKnownPropertyNameSet() { + return VCardParser_V21.sKnownPropertyNameSet; + } + + protected Set getKnownTypeSet() { + return VCardParser_V21.sKnownTypeSet; + } + + protected Set getKnownValueSet() { + return VCardParser_V21.sKnownValueSet; + } + + protected Set 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 000000000..61d045598 --- /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; + +/** + *

+ * Basic implementation achieving vCard 3.0 parsing. + *

+ *

+ * 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}. + *

+ * @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 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 000000000..2a5e31326 --- /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; + +/** + *

+ * vCard parser for vCard 2.1. See the specification for more detail about the spec itself. + *

+ *

+ * 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. + *

+ * 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). + *

+ */ +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 sKnownPropertyNameSet = + Collections.unmodifiableSet(new HashSet( + 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 sKnownTypeSet = + Collections.unmodifiableSet(new HashSet( + 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 sKnownValueSet = + Collections.unmodifiableSet(new HashSet( + Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID"))); + + /** + *

+ * A unmodifiable Set storing the values for the type "ENCODING", available in the vCard 2.1. + *

+ *

+ * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. + * We allow it for safety. + *

+ */ + /* package */ static final Set sAvailableEncoding = + Collections.unmodifiableSet(new HashSet( + 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 000000000..179869b21 --- /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; + +/** + *

+ * vCard parser for vCard 3.0. See RFC 2426 for more detail. + *

+ *

+ * 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. + *

+ *

+ * 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". + *

+ */ +public class VCardParser_V30 implements VCardParser { + /* package */ static final Set sKnownPropertyNameSet = + Collections.unmodifiableSet(new HashSet(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 + + /** + *

+ * A unmodifiable Set storing the values for the type "ENCODING", available in the vCard 3.0. + *

+ *

+ * Though vCard 2.1 specification does not allow "7BIT" or "BASE64", we allow them for safety. + *

+ *

+ * "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. + *

+ */ + /* package */ static final Set sAcceptableEncoding = + Collections.unmodifiableSet(new HashSet(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 000000000..e70d4961c --- /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; + +/** + *

+ * The class which tries to detects the source of a vCard file from its contents. + *

+ *

+ * 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. + *

+ *

+ * 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). + *

+ */ +public class VCardSourceDetector implements VCardInterpreter { + private static Set APPLE_SIGNS = new HashSet(Arrays.asList( + "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", "X-PHONETIC-LAST-NAME", + "X-ABADR", "X-ABUID")); + + private static Set JAPANESE_MOBILE_PHONE_SIGNS = new HashSet(Arrays.asList( + "X-GNO", "X-GN", "X-REDUCTION")); + + private static Set WINDOWS_MOBILE_PHONE_SIGNS = new HashSet(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 FOMA_SIGNS = new HashSet(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 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; + } + } + + /** + *

+ * Returns charset String guessed from the source's properties. + * This method must be called after parsing target file(s). + *

+ * @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 000000000..fb0c2e7b6 --- /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 sKnownPhoneTypesMap_ItoS; + private static final Set sPhoneTypesUnknownToContactsSet; + private static final Map sKnownPhoneTypeMap_StoI; + private static final Map sKnownImPropNameMap_ItoS; + private static final Set sMobilePhoneLabelSet; + + static { + sKnownPhoneTypesMap_ItoS = new HashMap(); + sKnownPhoneTypeMap_StoI = new HashMap(); + + 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(); + 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(); + 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(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 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; + } + } + + /** + *

+ * Inserts postal data into the builder object. + *

+ *

+ * Note that the data structure of ContactsContract is different from that defined in vCard. + * So some conversion may be performed in this method. + *

+ */ + 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 constructListFromValue(final String value, + final boolean isV30) { + final List list = new ArrayList(); + 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 values) { + if (values == null) { + return true; + } + for (final String value : values) { + if (TextUtils.isEmpty(value)) { + continue; + } + if (!TextUtils.isPrintableAsciiOnly(value)) { + return false; + } + } + return true; + } + + /** + *

+ * This is useful when checking the string should be encoded into quoted-printable + * or not, which is required by vCard 2.1. + *

+ *

+ * See the definition of "7bit" in vCard 2.1 spec for more information. + *

+ */ + public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) { + if (values == null) { + return true; + } + return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values)); + } + + public static boolean containsOnlyNonCrLfPrintableAscii(final Collection 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 sUnAcceptableAsciiInV21WordSet = + new HashSet(Arrays.asList('[', ']', '=', ':', '.', ',', ' ')); + + /** + *

+ * This is useful since vCard 3.0 often requires the ("X-") properties and groups + * should contain only alphabets, digits, and hyphen. + *

+ *

+ * 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. + *

+ */ + public static boolean containsOnlyAlphaDigitHyphen(final String...values) { + if (values == null) { + return true; + } + return containsOnlyAlphaDigitHyphen(Arrays.asList(values)); + } + + public static boolean containsOnlyAlphaDigitHyphen(final Collection 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; + } + + /** + *

+ * Returns true when the given String is categorized as "word" specified in vCard spec 2.1. + *

+ *

+ * vCard 2.1 specifies:
+ * word = <any printable 7bit us-ascii except []=:., > + *

+ */ + 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 list = new ArrayList(); + 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 000000000..c408716e5 --- /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 000000000..3ad7fd38b --- /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 000000000..342769ef5 --- /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 000000000..5c2250fc3 --- /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 000000000..2b9b1acf5 --- /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 000000000..61ff752c9 --- /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 000000000..047c58053 --- /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); + } +} diff --git a/vcard/tests/Android.mk b/vcard/tests/Android.mk new file mode 100644 index 000000000..853ee1451 --- /dev/null +++ b/vcard/tests/Android.mk @@ -0,0 +1,25 @@ +# 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_CERTIFICATE := platform +LOCAL_MODULE_TAGS := tests +LOCAL_PACKAGE_NAME := AndroidVCardTests +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_JAVA_LIBRARIES := android.test.runner google-common +LOCAL_STATIC_JAVA_LIBRARIES := com.android.vcard + +include $(BUILD_PACKAGE) diff --git a/vcard/tests/AndroidManifest.xml b/vcard/tests/AndroidManifest.xml new file mode 100644 index 000000000..fcbf76721 --- /dev/null +++ b/vcard/tests/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/vcard/tests/res/raw/v21_backslash.vcf b/vcard/tests/res/raw/v21_backslash.vcf new file mode 100644 index 000000000..bd3002b32 --- /dev/null +++ b/vcard/tests/res/raw/v21_backslash.vcf @@ -0,0 +1,5 @@ +BEGIN:VCARD +VERSION:2.1 +N:;A\;B\\;C\\\;;D;\:E;\\\\; +FN:A;B\C\;D:E\\ +END:VCARD diff --git a/vcard/tests/res/raw/v21_complicated.vcf b/vcard/tests/res/raw/v21_complicated.vcf new file mode 100644 index 000000000..de34e1668 --- /dev/null +++ b/vcard/tests/res/raw/v21_complicated.vcf @@ -0,0 +1,106 @@ +BEGIN:VCARD +VERSION:2.1 +N:Gump;Forrest;Hoge;Pos;Tao +FN:Joe Due +ORG:Gump Shrimp Co.;Sales Dept.\;Manager;Fish keeper +ROLE:Fish Cake Keeper! +X-CLASS:PUBLIC +TITLE:Shrimp Man +TEL;WORK;VOICE:(111) 555-1212 +TEL;HOME;VOICE:(404) 555-1212 +TEL;CELL:0311111111 +TEL;VIDEO:0322222222 +TEL;VOICE:0333333333 +ADR;WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America +LABEL;WORK;ENCODING=QUOTED-PRINTABLE:100 Waters Edge=0D=0ABaytown, LA 30314=0D=0AUnited States of America +ADR;HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America +LABEL;HOME;ENCODING=QUOTED-PRINTABLE:42 Plantation St.=0D=0A= +Baytown, LA 30314=0D=0A= +United States of America +EMAIL;PREF;INTERNET:forrestgump@walladalla.com +EMAIL;CELL:cell@example.com +NOTE:The following note is the example from RFC 2045. +NOTE;ENCODING=QUOTED-PRINTABLE:Now's the time = +for all folk to come= + to the aid of their country. + +PHOTO;ENCODING=BASE64;TYPE=JPEG: + /9j/4QoPRXhpZgAATU0AKgAAAAgADQEOAAIAAAAPAAAAqgEPAAIAAAAHAAAAugEQAAIAAAAG + AAAAwgESAAMAAAABAAEAAAEaAAUAAAABAAAAyAEbAAUAAAABAAAA0AEoAAMAAAABAAIAAAEx + AAIAAAAOAAAA2AEyAAIAAAAUAAAA5gITAAMAAAABAAEAAIKYAAIAAAAOAAAA+odpAAQAAAAB + AAABhMSlAAcAAAB8AAABCAAABB4yMDA4MTAyOTEzNTUzMQAARG9Db01vAABEOTA1aQAAAABI + AAAAAQAAAEgAAAABRDkwNWkgVmVyMS4wMAAyMDA4OjEwOjI5IDEzOjU1OjQ3ACAgICAgICAg + ICAgICAAUHJpbnRJTQAwMzAwAAAABgABABQAFAACAQAAAAADAAAANAEABQAAAAEBAQAAAAEQ + gAAAAAAAEQkAACcQAAAPCwAAJxAAAAWXAAAnEAAACLAAACcQAAAcAQAAJxAAAAJeAAAnEAAA + AIsAACcQAAADywAAJxAAABvlAAAnEAAogpoABQAAAAEAAANqgp0ABQAAAAEAAANyiCIAAwAA + AAEAAgAAkAAABwAAAAQwMjIwkAMAAgAAABQAAAN6kAQAAgAAABQAAAOOkQEABwAAAAQBAgMA + kQIABQAAAAEAAAOikgEACgAAAAEAAAOqkgIABQAAAAEAAAOykgQACgAAAAEAAAO6kgUABQAA + AAEAAAPCkgcAAwAAAAEAAgAAkggAAwAAAAEAAAAAkgkAAwAAAAEAAAAAkgoABQAAAAEAAAPK + knwABwAAAAEAAAAAkoYABwAAABYAAAPSoAAABwAAAAQwMTAwoAEAAwAAAAEAAQAAoAIAAwAA + AAEAYAAAoAMAAwAAAAEASAAAoAUABAAAAAEAAAQAog4ABQAAAAEAAAPoog8ABQAAAAEAAAPw + ohAAAwAAAAEAAgAAohcAAwAAAAEAAgAAowAABwAAAAEDAAAAowEABwAAAAEBAAAApAEAAwAA + AAEAAAAApAIAAwAAAAEAAAAApAMAAwAAAAEAAAAApAQABQAAAAEAAAP4pAUAAwAAAAEAHQAA + pAYAAwAAAAEAAAAApAcAAwAAAAEAAAAApAgAAwAAAAEAAAAApAkAAwAAAAEAAAAApAoAAwAA + AAEAAAAApAwAAwAAAAEAAgAAAAAAAAAAAFMAACcQAAABXgAAAGQyMDA4OjEwOjI5IDEzOjU1 + OjMxADIwMDg6MTA6MjkgMTM6NTU6NDcAAAApiAAAGwAAAAKyAAAAZAAAAV4AAABkAAAAAAAA + AGQAAAAlAAAACgAADpIAAAPoAAAAAAAAAAAyMDA4MTAyOTEzNTUzMQAAICoAAAAKAAAq4gAA + AAoAAAAAAAAAAQACAAEAAgAAAARSOTgAAAIABwAAAAQwMTAwAAAAAAAGAQMAAwAAAAEABgAA + ARoABQAAAAEAAARsARsABQAAAAEAAAR0ASgAAwAAAAEAAgAAAgEABAAAAAEAAAR8AgIABAAA + AAEAAAWLAAAAAAAAAEgAAAABAAAASAAAAAH/2P/bAIQAIBYYHBgUIBwaHCQiICYwUDQwLCww + YkZKOlB0Znp4cmZwboCQuJyAiK6KbnCg2qKuvsTO0M58muLy4MjwuMrOxgEiJCQwKjBeNDRe + xoRwhMbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbG + /8AAEQgAeACgAwEhAAIRAQMRAf/EAaIAAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKCxAA + AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK + FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG + h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl + 5ufo6erx8vP09fb3+Pn6AQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgsRAAIBAgQEAwQH + BQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBka + JicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKT + lJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz + 9PX29/j5+v/aAAwDAQACEQMRAD8AFFSqKkZIoqRVpgSKKeBTEOApwFADsUYpgIRSEUANIppF + ICNhUTCgCMio2FICJhULCgC0oqVaAJFFSqKBkgFOApiHCnCgB2KMUCENJQA0imEUDGMKiYUA + RtUbUgIWqJhQBZSpFoAlWpVoGPFPFMQ7tSK2ODQA4yKO9HmKe9FxAzDHFIOlAAaYaAGNUTUD + ImqNqQETVE1AE6VKKAJFNSqaAHg08GmANIFFQM5Y5qJMBuT60ZNQIcrkVYSQMKuLGKaaasQx + qiagZE1RtSAjaomoAkQ1KpoAlU1IpoAkU07OBTArO+5qkV12Y71lfUBmaKkCRSuznrTFba2a + oCwGyM0E1qIjY1GxoGRNUZNICNqiagByGplNAEimpFNMB4YDvSucpxSYEIU04KazsAu1qArU + WELtPpTSposBNETt5pxNaoCNjUbGgCNjUZoGRtUTUgFU1KpoAkBqQHigCFnO7rUqOdlZp6gA + c+tODn1pXAXzD60eYfWncQvmNSGQ07gOMhCVEJGz1ptgS5yKYxqwGE1GxoAiamGkMapqVTQB + Kpp+eKAICfmqWM/Kaz6gANOBqQFzRmmAuaTNACsfkqMHmm9wJs8U0mtRDGNRsaAI2phpDI1N + SqaAJFNSA8UCISfmqSM/Kaz6jAHmnA1ICg0uaAFzSZpgKx+SmDrTe4E2eKaTWoiMmmMaAIzT + DSGRKakU0ASKaeDTERseakjPyms+oxAacDUgOBpc0gFzSZpgOY/KKYv3qrqIlpprQBjGoyaA + GGmmkMgU1IppgPBqQGgQu0Gn4wvFKwEQpwNZDHZpc0ALmigRKBleaQKBWtgA001QDGqM0gGm + mGkMrqakBoAepp4NMRIDTwaAE2A008GokgHxjd1pzKFpW0uAg5NSBBTirgOpDWgDTTTQAw0w + 0gGGmmgZWBp4pASKaeDTEOBp4NADwajbrUyBEkXWnSUdAGr1qeiAMSkNWAhphoAaaYaQDDTT + SGVRTwaYDxTwaBDwaeDQA4GlK5oauIeo20pGaLaAKqgU6hKwBSGmAhphoAaaYaQxhpppDKgN + PFMB4p4oEPFOBpgPBp4NAhwpwoAWloAKSgBDTTQMYaYaQDTTTSGA/9n/2wCEAAoHBwgHBgoI + CAgLCgoLDhgQDg0NDh0VFhEYIx8lJCIfIiEmKzcvJik0KSEiMEExNDk7Pj4+JS5ESUM8SDc9 + PjsBCgsLDg0OHBAQHDsoIig7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7 + Ozs7Ozs7Ozs7Ozs7O//AABEIAEgAYAMBIQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAA + AQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNC + scEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp + anN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS + 09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcI + CQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVi + ctEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4 + eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY + 2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AJ7SLgcVr20I4rNFGvbQAAHFaEUX + SrQi5HCMdKk8oY6VSJYx4hjpVaWMelAFC4iGDxWPdR8mkxmRdxjBrEvI+tZjN20xtHNbNqAc + UIDXg6Cr0WKtCY8XKQOyzOB3FKNWsyceZ+lS6sY6NkjvPSdwImBHUmmy4q076oCjOODWPdgc + 0MpGPdAYNYl4o5rNjNKzkyorZtXxihAa1vIDip7m9Frb7/4jwKcnyxbEzN3ieJppZsyZ4U1H + urzZau4mWVlNrGk0UuWPVa1YroXEIkHfrXZh5W90RWncAHmsi6bJNdQ0ZNw3BrGuiMGs2Mks + puBzWzbzdOaEBeOpR2oUtkk9hTru7iuo4m8wgemKyqTi04sBsfkEf68j8KlUQZz9o/SuZRj3 + JYriAji4/Sp7W6htbV2aXcu70ramoxle4gN7HcIXjbis+4k5NdaaauhmVcv1rHuW61DGiG1m + 6c1s20/TmgAv5vmj57VKk3+ixnPc1xVV70h9CVJuOtSrL71hFgxzScUkkn+iY/2q1i9xDrGT + 9y31pJ5Otd1L+GhMy7mTrWXO2SapjRn28vTmta3nxjmgGOvJd2w1Kkv+ipz/ABGuOoveYdCe + ObjrU6y5rlsA8ycUksn+ij/eNaw6iJLNsW59zTJn6816FP4EJmbO+Saz5m602UjIgk4HNadv + LwKaBl+MpIMOMipp490SCJeF7CoqQvF2JuRqWQ4YEGrSiQJuKnHrXByMpki73GFBNXIoh9n2 + SrnnOK6MPTbd3sSwIVF2qMCqkzHmuy1lYRnTHrVGWpZaMKB+BWlbycYoQM0IZDxzV+GU8c1a + IYy5Y+dnHatAsfsAHfArmS1mPoh1gT8x9qtk1rQX7tCe5DIapzGtGBQm71SlqGWjnIH6Vowt + zmhAy/E3vV6F6tEMuxlWIyAfrVxCCAO1VZEEyYA4AApxNGwyJ+lVJRUsaKMw61SlFQzRAP/Z + +X-ATTRIBUTE:Some String +BDAY:19800101 +GEO:35.6563854,139.6994233 +URL:http://www.example.com/ +REV:20080424T195243Z +END:VCARD \ No newline at end of file diff --git a/vcard/tests/res/raw/v21_invalid_comment_line.vcf b/vcard/tests/res/raw/v21_invalid_comment_line.vcf new file mode 100644 index 000000000..f910710af --- /dev/null +++ b/vcard/tests/res/raw/v21_invalid_comment_line.vcf @@ -0,0 +1,10 @@ +BEGIN:vCard +VERSION:2.1 +UID:357 +N:;Conference Call +FN:Conference Call +# This line must be ignored. +NOTE;ENCODING=QUOTED-PRINTABLE:This is an (sharp ->= +#<- sharp) example. This message must NOT be ignored. +# This line must be ignored too. +END:vCard diff --git a/vcard/tests/res/raw/v21_japanese_1.vcf b/vcard/tests/res/raw/v21_japanese_1.vcf new file mode 100644 index 000000000..d05e2fffb --- /dev/null +++ b/vcard/tests/res/raw/v21_japanese_1.vcf @@ -0,0 +1,6 @@ +BEGIN:VCARD +VERSION:2.1 +N;CHARSET=SHIFT_JIS:ˆÀ“¡ƒƒCƒh;;;; +SOUND;X-IRMC-N;CHARSET=SHIFT_JIS:±ÝÄÞ³Û²ÄÞ;;;; +TEL;PREF;VOICE:0300000000 +END:VCARD diff --git a/vcard/tests/res/raw/v21_japanese_2.vcf b/vcard/tests/res/raw/v21_japanese_2.vcf new file mode 100644 index 000000000..fa54acbc8 --- /dev/null +++ b/vcard/tests/res/raw/v21_japanese_2.vcf @@ -0,0 +1,10 @@ +BEGIN:VCARD +VERSION:2.1 +FN;CHARSET=SHIFT_JIS:ˆÀ“¡ ƒƒCƒh 1 +N;CHARSET=SHIFT_JIS:ˆÀ“¡;ƒƒCƒh1;;; +SOUND;X-IRMC-N;CHARSET=SHIFT_JIS:±ÝÄÞ³;Û²ÄÞ1;;; +ADR;HOME;CHARSET=SHIFT_JIS;ENCODING=QUOTED-PRINTABLE:;=93=8C=8B=9E=93=73= +=8F=61=92=4A=8B=E6=8D=F7=8B=75=92=AC26-1=83=5A=83=8B=83=8A=83=41=83=93= +=83=5E=83=8F=81=5B6=8A=4B;;;;150-8512; +NOTE;CHARSET=SHIFT_JIS;ENCODING=QUOTED-PRINTABLE:=83=81=83=82 +END:VCARD diff --git a/vcard/tests/res/raw/v21_multiple_entry.vcf b/vcard/tests/res/raw/v21_multiple_entry.vcf new file mode 100644 index 000000000..ebbb19a4b --- /dev/null +++ b/vcard/tests/res/raw/v21_multiple_entry.vcf @@ -0,0 +1,33 @@ +BEGIN:VCARD +VERSION:2.1 +N;CHARSET=SHIFT_JIS:ˆÀ“¡ƒƒCƒh3;;;; +SOUND;X-IRMC-N;CHARSET=SHIFT_JIS:±ÝÄÞ³Û²ÄÞ3;;;; +TEL;X-NEC-SECRET:9 +TEL;X-NEC-HOTEL:10 +TEL;X-NEC-SCHOOL:11 +TEL;HOME;FAX:12 +END:VCARD + + +BEGIN:VCARD +VERSION:2.1 +N;CHARSET=SHIFT_JIS:ˆÀ“¡ƒƒCƒh4;;;; +SOUND;X-IRMC-N;CHARSET=SHIFT_JIS:±ÝÄÞ³Û²ÄÞ4;;;; +TEL;MODEM:13 +TEL;PAGER:14 +TEL;X-NEC-FAMILY:15 +TEL;X-NEC-GIRL:16 +END:VCARD + + +BEGIN:VCARD +VERSION:2.1 +N;CHARSET=SHIFT_JIS:ˆÀ“¡ƒƒCƒh5;;;; +SOUND;X-IRMC-N;CHARSET=SHIFT_JIS:±ÝÄÞ³Û²ÄÞ5;;;; +TEL;X-NEC-BOY:17 +TEL;X-NEC-FRIEND:18 +TEL;X-NEC-PHS:19 +TEL;X-NEC-RESTAURANT:20 +END:VCARD + + diff --git a/vcard/tests/res/raw/v21_org_before_title.vcf b/vcard/tests/res/raw/v21_org_before_title.vcf new file mode 100644 index 000000000..8ff1190f1 --- /dev/null +++ b/vcard/tests/res/raw/v21_org_before_title.vcf @@ -0,0 +1,6 @@ +BEGIN:VCARD +VERSION:2.1 +FN:Normal Guy +ORG:Company;Organization;Devision;Room;Sheet No. +TITLE:Excellent Janitor +END:VCARD diff --git a/vcard/tests/res/raw/v21_pref_handling.vcf b/vcard/tests/res/raw/v21_pref_handling.vcf new file mode 100644 index 000000000..51053101a --- /dev/null +++ b/vcard/tests/res/raw/v21_pref_handling.vcf @@ -0,0 +1,15 @@ +BEGIN:VCARD +VERSION:2.1 +FN:Smith +TEL;HOME:1 +TEL;WORK;PREF:2 +TEL;ISDN:3 +EMAIL;PREF;HOME:test@example.com +EMAIL;CELL;PREF:test2@examination.com +ORG:Company +TITLE:Engineer +ORG:Mystery +TITLE:Blogger +ORG:Poetry +TITLE:Poet +END:VCARD diff --git a/vcard/tests/res/raw/v21_simple_1.vcf b/vcard/tests/res/raw/v21_simple_1.vcf new file mode 100644 index 000000000..6aabb4c02 --- /dev/null +++ b/vcard/tests/res/raw/v21_simple_1.vcf @@ -0,0 +1,3 @@ +BEGIN:VCARD +N:Ando;Roid; +END:VCARD diff --git a/vcard/tests/res/raw/v21_simple_2.vcf b/vcard/tests/res/raw/v21_simple_2.vcf new file mode 100644 index 000000000..f0d5ab506 --- /dev/null +++ b/vcard/tests/res/raw/v21_simple_2.vcf @@ -0,0 +1,3 @@ +BEGIN:VCARD +FN:Ando Roid +END:VCARD diff --git a/vcard/tests/res/raw/v21_simple_3.vcf b/vcard/tests/res/raw/v21_simple_3.vcf new file mode 100644 index 000000000..beddabb96 --- /dev/null +++ b/vcard/tests/res/raw/v21_simple_3.vcf @@ -0,0 +1,4 @@ +BEGIN:VCARD +N:Ando;Roid; +FN:Ando Roid +END:VCARD diff --git a/vcard/tests/res/raw/v21_title_before_org.vcf b/vcard/tests/res/raw/v21_title_before_org.vcf new file mode 100644 index 000000000..9fdc7389c --- /dev/null +++ b/vcard/tests/res/raw/v21_title_before_org.vcf @@ -0,0 +1,6 @@ +BEGIN:VCARD +VERSION:2.1 +FN:Nice Guy +TITLE:Cool Title +ORG:Marverous;Perfect;Great;Good;Bad;Poor +END:VCARD diff --git a/vcard/tests/res/raw/v21_winmo_65.vcf b/vcard/tests/res/raw/v21_winmo_65.vcf new file mode 100644 index 000000000..f380d0d5c --- /dev/null +++ b/vcard/tests/res/raw/v21_winmo_65.vcf @@ -0,0 +1,10 @@ +BEGIN:VCARD +VERSION:2.1 +N:Example;;;; +FN:Example +ANNIVERSARY;VALUE=DATE:20091010 +AGENT:Invalid line which must be handled correctly. +X-CLASS:PUBLIC +X-REDUCTION: +X-NO: +END:VCARD diff --git a/vcard/tests/res/raw/v30_comma_separated.vcf b/vcard/tests/res/raw/v30_comma_separated.vcf new file mode 100644 index 000000000..98a7f2058 --- /dev/null +++ b/vcard/tests/res/raw/v30_comma_separated.vcf @@ -0,0 +1,5 @@ +BEGIN:VCARD +VERSION:3.0 +N:F;G;M;; +TEL;TYPE=PAGER,WORK,MSG:6101231234@pagersample.com +END:VCARD diff --git a/vcard/tests/res/raw/v30_simple.vcf b/vcard/tests/res/raw/v30_simple.vcf new file mode 100644 index 000000000..418661f74 --- /dev/null +++ b/vcard/tests/res/raw/v30_simple.vcf @@ -0,0 +1,13 @@ +BEGIN:VCARD +VERSION:3.0 +FN:And Roid +N:And;Roid;;; +ORG:Open;Handset; Alliance +SORT-STRING:android +TEL;TYPE=PREF;TYPE=VOICE:0300000000 +CLASS:PUBLIC +X-GNO:0 +X-GN:group0 +X-REDUCTION:0 +REV:20081031T065854Z +END:VCARD diff --git a/vcard/tests/src/com/android/vcard/tests/VCardExporterTests.java b/vcard/tests/src/com/android/vcard/tests/VCardExporterTests.java new file mode 100644 index 000000000..b6419c36f --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/VCardExporterTests.java @@ -0,0 +1,971 @@ +/* + * 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.tests; + +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 com.android.vcard.VCardConfig; +import com.android.vcard.tests.test_utils.ContactEntry; +import com.android.vcard.tests.test_utils.PropertyNodesVerifierElem; +import com.android.vcard.tests.test_utils.PropertyNodesVerifierElem.TypeSet; + +import java.util.Arrays; + +/** + * Tests for the code related to vCard exporter, inculding vCard composer. + * This test class depends on vCard importer code, so if tests for vCard importer fail, + * the result of this class will not be reliable. + */ +public class VCardExporterTests extends VCardTestsBase { + private static final byte[] sPhotoByteArray = + VCardImporterTests.sPhotoByteArrayForComplicatedCase; + + public void testSimpleV21() { + mVerifier.initForExportTest(V21); + mVerifier.addInputEntry().addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Ando") + .put(StructuredName.GIVEN_NAME, "Roid"); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNode("FN", "Roid Ando") + .addExpectedNode("N", "Ando;Roid;;;", + Arrays.asList("Ando", "Roid", "", "", "")); + } + + private void testStructuredNameBasic(int vcardType) { + final boolean isV30 = VCardConfig.isV30(vcardType); + mVerifier.initForExportTest(vcardType); + mVerifier.addInputEntry().addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName") + .put(StructuredName.GIVEN_NAME, "AppropriateGivenName") + .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName") + .put(StructuredName.PREFIX, "AppropriatePrefix") + .put(StructuredName.SUFFIX, "AppropriateSuffix") + .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily") + .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle"); + + PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("N", + "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" + + "AppropriatePrefix;AppropriateSuffix", + Arrays.asList("AppropriateFamilyName", "AppropriateGivenName", + "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix")) + .addExpectedNodeWithOrder("FN", + "AppropriatePrefix AppropriateGivenName " + + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix") + .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven") + .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle") + .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily"); + + if (isV30) { + elem.addExpectedNode("SORT-STRING", + "AppropriatePhoneticGiven AppropriatePhoneticMiddle " + + "AppropriatePhoneticFamily"); + } + } + + public void testStructuredNameBasicV21() { + testStructuredNameBasic(V21); + } + + public void testStructuredNameBasicV30() { + testStructuredNameBasic(V30); + } + + /** + * Test that only "primary" StructuredName is emitted, so that our vCard file + * will not confuse the external importer, assuming there may be some importer + * which presume that there's only one property toward each of "N", "FN", etc. + * Note that more than one "N", "FN", etc. properties are acceptable in vCard spec. + */ + private void testStructuredNameUsePrimaryCommon(int vcardType) { + final boolean isV30 = (vcardType == V30); + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName1") + .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName1") + .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName1") + .put(StructuredName.PREFIX, "DoNotEmitPrefix1") + .put(StructuredName.SUFFIX, "DoNotEmitSuffix1") + .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily1") + .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven1") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle1"); + + // With "IS_PRIMARY=1". This is what we should use. + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName") + .put(StructuredName.GIVEN_NAME, "AppropriateGivenName") + .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName") + .put(StructuredName.PREFIX, "AppropriatePrefix") + .put(StructuredName.SUFFIX, "AppropriateSuffix") + .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily") + .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle") + .put(StructuredName.IS_PRIMARY, 1); + + // With "IS_PRIMARY=1", but we should ignore this time, since this is second, not first. + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName2") + .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName2") + .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName2") + .put(StructuredName.PREFIX, "DoNotEmitPrefix2") + .put(StructuredName.SUFFIX, "DoNotEmitSuffix2") + .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily2") + .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven2") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle2") + .put(StructuredName.IS_PRIMARY, 1); + + PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("N", + "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" + + "AppropriatePrefix;AppropriateSuffix", + Arrays.asList("AppropriateFamilyName", "AppropriateGivenName", + "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix")) + .addExpectedNodeWithOrder("FN", + "AppropriatePrefix AppropriateGivenName " + + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix") + .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven") + .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle") + .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily"); + + if (isV30) { + elem.addExpectedNode("SORT-STRING", + "AppropriatePhoneticGiven AppropriatePhoneticMiddle " + + "AppropriatePhoneticFamily"); + } + } + + public void testStructuredNameUsePrimaryV21() { + testStructuredNameUsePrimaryCommon(V21); + } + + public void testStructuredNameUsePrimaryV30() { + testStructuredNameUsePrimaryCommon(V30); + } + + /** + * Tests that only "super primary" StructuredName is emitted. + * See also the comment in {@link #testStructuredNameUsePrimaryCommon(int)}. + */ + private void testStructuredNameUseSuperPrimaryCommon(int vcardType) { + final boolean isV30 = (vcardType == V30); + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName1") + .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName1") + .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName1") + .put(StructuredName.PREFIX, "DoNotEmitPrefix1") + .put(StructuredName.SUFFIX, "DoNotEmitSuffix1") + .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily1") + .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven1") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle1"); + + // With "IS_PRIMARY=1", but we should ignore this time. + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName2") + .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName2") + .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName2") + .put(StructuredName.PREFIX, "DoNotEmitPrefix2") + .put(StructuredName.SUFFIX, "DoNotEmitSuffix2") + .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily2") + .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven2") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle2") + .put(StructuredName.IS_PRIMARY, 1); + + // With "IS_SUPER_PRIMARY=1". This is what we should use. + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName") + .put(StructuredName.GIVEN_NAME, "AppropriateGivenName") + .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName") + .put(StructuredName.PREFIX, "AppropriatePrefix") + .put(StructuredName.SUFFIX, "AppropriateSuffix") + .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily") + .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle") + .put(StructuredName.IS_SUPER_PRIMARY, 1); + + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName3") + .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName3") + .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName3") + .put(StructuredName.PREFIX, "DoNotEmitPrefix3") + .put(StructuredName.SUFFIX, "DoNotEmitSuffix3") + .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily3") + .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven3") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle3") + .put(StructuredName.IS_PRIMARY, 1); + + PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("N", + "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" + + "AppropriatePrefix;AppropriateSuffix", + Arrays.asList("AppropriateFamilyName", "AppropriateGivenName", + "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix")) + .addExpectedNodeWithOrder("FN", + "AppropriatePrefix AppropriateGivenName " + + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix") + .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven") + .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle") + .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily"); + + if (isV30) { + elem.addExpectedNode("SORT-STRING", + "AppropriatePhoneticGiven AppropriatePhoneticMiddle" + + " AppropriatePhoneticFamily"); + } + } + + public void testStructuredNameUseSuperPrimaryV21() { + testStructuredNameUseSuperPrimaryCommon(V21); + } + + public void testStructuredNameUseSuperPrimaryV30() { + testStructuredNameUseSuperPrimaryCommon(V30); + } + + public void testNickNameV30() { + mVerifier.initForExportTest(V30); + mVerifier.addInputEntry().addContentValues(Nickname.CONTENT_ITEM_TYPE) + .put(Nickname.NAME, "Nicky"); + + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNodeWithOrder("NICKNAME", "Nicky"); + } + + private void testPhoneBasicCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + mVerifier.addInputEntry().addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "1") + .put(Phone.TYPE, Phone.TYPE_HOME); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "1", new TypeSet("HOME")); + } + + public void testPhoneBasicV21() { + testPhoneBasicCommon(V21); + } + + public void testPhoneBasicV30() { + testPhoneBasicCommon(V30); + } + + public void testPhoneRefrainFormatting() { + mVerifier.initForExportTest(V21 | VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING); + mVerifier.addInputEntry().addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "1234567890(abcdefghijklmnopqrstuvwxyz)") + .put(Phone.TYPE, Phone.TYPE_HOME); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "1234567890(abcdefghijklmnopqrstuvwxyz)", + new TypeSet("HOME")); + } + + /** + * Tests that vCard composer emits corresponding type param which we expect. + */ + private void testPhoneVariousTypeSupport(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "10") + .put(Phone.TYPE, Phone.TYPE_HOME); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "20") + .put(Phone.TYPE, Phone.TYPE_WORK); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "30") + .put(Phone.TYPE, Phone.TYPE_FAX_HOME); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "40") + .put(Phone.TYPE, Phone.TYPE_FAX_WORK); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "50") + .put(Phone.TYPE, Phone.TYPE_MOBILE); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "60") + .put(Phone.TYPE, Phone.TYPE_PAGER); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "70") + .put(Phone.TYPE, Phone.TYPE_OTHER); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "80") + .put(Phone.TYPE, Phone.TYPE_CAR); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "90") + .put(Phone.TYPE, Phone.TYPE_COMPANY_MAIN); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "100") + .put(Phone.TYPE, Phone.TYPE_ISDN); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "110") + .put(Phone.TYPE, Phone.TYPE_MAIN); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "120") + .put(Phone.TYPE, Phone.TYPE_OTHER_FAX); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "130") + .put(Phone.TYPE, Phone.TYPE_TELEX); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "140") + .put(Phone.TYPE, Phone.TYPE_WORK_MOBILE); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "150") + .put(Phone.TYPE, Phone.TYPE_WORK_PAGER); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "160") + .put(Phone.TYPE, Phone.TYPE_MMS); + + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "10", new TypeSet("HOME")) + .addExpectedNode("TEL", "20", new TypeSet("WORK")) + .addExpectedNode("TEL", "30", new TypeSet("HOME", "FAX")) + .addExpectedNode("TEL", "40", new TypeSet("WORK", "FAX")) + .addExpectedNode("TEL", "50", new TypeSet("CELL")) + .addExpectedNode("TEL", "60", new TypeSet("PAGER")) + .addExpectedNode("TEL", "70", new TypeSet("VOICE")) + .addExpectedNode("TEL", "80", new TypeSet("CAR")) + .addExpectedNode("TEL", "90", new TypeSet("WORK", "PREF")) + .addExpectedNode("TEL", "100", new TypeSet("ISDN")) + .addExpectedNode("TEL", "110", new TypeSet("PREF")) + .addExpectedNode("TEL", "120", new TypeSet("FAX")) + .addExpectedNode("TEL", "130", new TypeSet("TLX")) + .addExpectedNode("TEL", "140", new TypeSet("WORK", "CELL")) + .addExpectedNode("TEL", "150", new TypeSet("WORK", "PAGER")) + .addExpectedNode("TEL", "160", new TypeSet("MSG")); + } + + public void testPhoneVariousTypeSupportV21() { + testPhoneVariousTypeSupport(V21); + } + + public void testPhoneVariousTypeSupportV30() { + testPhoneVariousTypeSupport(V30); + } + + /** + * Tests that "PREF"s are emitted appropriately. + */ + private void testPhonePrefHandlingCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "1") + .put(Phone.TYPE, Phone.TYPE_HOME); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "2") + .put(Phone.TYPE, Phone.TYPE_WORK) + .put(Phone.IS_PRIMARY, 1); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "3") + .put(Phone.TYPE, Phone.TYPE_FAX_HOME) + .put(Phone.IS_PRIMARY, 1); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "4") + .put(Phone.TYPE, Phone.TYPE_FAX_WORK); + + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "4", new TypeSet("WORK", "FAX")) + .addExpectedNode("TEL", "3", new TypeSet("HOME", "FAX", "PREF")) + .addExpectedNode("TEL", "2", new TypeSet("WORK", "PREF")) + .addExpectedNode("TEL", "1", new TypeSet("HOME")); + } + + public void testPhonePrefHandlingV21() { + testPhonePrefHandlingCommon(V21); + } + + public void testPhonePrefHandlingV30() { + testPhonePrefHandlingCommon(V30); + } + + private void testMiscPhoneTypeHandling(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "1") + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "Modem"); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "2") + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "MSG"); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "3") + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "BBS"); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "4") + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "VIDEO"); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "5") + .put(Phone.TYPE, Phone.TYPE_CUSTOM); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "6") + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "_AUTO_CELL"); // The old indicator for the type mobile. + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "7") + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "\u643A\u5E2F"); // Mobile phone in Japanese Kanji + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "8") + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "invalid"); + PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElemWithEmptyName(); + elem.addExpectedNode("TEL", "1", new TypeSet("MODEM")) + .addExpectedNode("TEL", "2", new TypeSet("MSG")) + .addExpectedNode("TEL", "3", new TypeSet("BBS")) + .addExpectedNode("TEL", "4", new TypeSet("VIDEO")) + .addExpectedNode("TEL", "5", new TypeSet("VOICE")) + .addExpectedNode("TEL", "6", new TypeSet("CELL")) + .addExpectedNode("TEL", "7", new TypeSet("CELL")) + .addExpectedNode("TEL", "8", new TypeSet("X-invalid")); + } + + public void testPhoneTypeHandlingV21() { + testMiscPhoneTypeHandling(V21); + } + + public void testPhoneTypeHandlingV30() { + testMiscPhoneTypeHandling(V30); + } + + private void testEmailBasicCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + mVerifier.addInputEntry().addContentValues(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "sample@example.com"); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("EMAIL", "sample@example.com"); + } + + public void testEmailBasicV21() { + testEmailBasicCommon(V21); + } + + public void testEmailBasicV30() { + testEmailBasicCommon(V30); + } + + private void testEmailVariousTypeSupportCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_home@example.com") + .put(Email.TYPE, Email.TYPE_HOME); + entry.addContentValues(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_work@example.com") + .put(Email.TYPE, Email.TYPE_WORK); + entry.addContentValues(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_mobile@example.com") + .put(Email.TYPE, Email.TYPE_MOBILE); + entry.addContentValues(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_other@example.com") + .put(Email.TYPE, Email.TYPE_OTHER); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("EMAIL", "type_home@example.com", new TypeSet("HOME")) + .addExpectedNode("EMAIL", "type_work@example.com", new TypeSet("WORK")) + .addExpectedNode("EMAIL", "type_mobile@example.com", new TypeSet("CELL")) + .addExpectedNode("EMAIL", "type_other@example.com"); + } + + public void testEmailVariousTypeSupportV21() { + testEmailVariousTypeSupportCommon(V21); + } + + public void testEmailVariousTypeSupportV30() { + testEmailVariousTypeSupportCommon(V30); + } + + private void testEmailPrefHandlingCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_home@example.com") + .put(Email.TYPE, Email.TYPE_HOME) + .put(Email.IS_PRIMARY, 1); + entry.addContentValues(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "type_notype@example.com") + .put(Email.IS_PRIMARY, 1); + + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("EMAIL", "type_notype@example.com", new TypeSet("PREF")) + .addExpectedNode("EMAIL", "type_home@example.com", new TypeSet("HOME", "PREF")); + } + + public void testEmailPrefHandlingV21() { + testEmailPrefHandlingCommon(V21); + } + + public void testEmailPrefHandlingV30() { + testEmailPrefHandlingCommon(V30); + } + + private void testPostalAddressCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.POBOX, "Pobox") + .put(StructuredPostal.NEIGHBORHOOD, "Neighborhood") + .put(StructuredPostal.STREET, "Street") + .put(StructuredPostal.CITY, "City") + .put(StructuredPostal.REGION, "Region") + .put(StructuredPostal.POSTCODE, "100") + .put(StructuredPostal.COUNTRY, "Country") + .put(StructuredPostal.FORMATTED_ADDRESS, "Formatted Address") + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK); + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal Code, + // ; Country Name + // + // The NEIGHBORHOOD field is appended after the CITY field. + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("ADR", + Arrays.asList("Pobox", "", "Street", "City Neighborhood", + "Region", "100", "Country"), new TypeSet("WORK")); + } + + public void testPostalAddressV21() { + testPostalAddressCommon(V21); + } + + public void testPostalAddressV30() { + testPostalAddressCommon(V30); + } + + private void testPostalAddressNonNeighborhood(int vcardType) { + mVerifier.initForExportTest(vcardType); + mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.CITY, "City"); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("ADR", + Arrays.asList("", "", "", "City", "", "", ""), new TypeSet("HOME")); + } + + public void testPostalAddressNonNeighborhoodV21() { + testPostalAddressNonNeighborhood(V21); + } + + public void testPostalAddressNonNeighborhoodV30() { + testPostalAddressNonNeighborhood(V30); + } + + private void testPostalAddressNonCity(int vcardType) { + mVerifier.initForExportTest(vcardType); + mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.NEIGHBORHOOD, "Neighborhood"); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("ADR", + Arrays.asList("", "", "", "Neighborhood", "", "", ""), new TypeSet("HOME")); + } + + public void testPostalAddressNonCityV21() { + testPostalAddressNonCity(V21); + } + + public void testPostalAddressNonCityV30() { + testPostalAddressNonCity(V30); + } + + private void testPostalOnlyWithFormattedAddressCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.REGION, "") // Must be ignored. + .put(StructuredPostal.FORMATTED_ADDRESS, + "Formatted address CA 123-334 United Statue"); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNodeWithOrder("ADR", ";Formatted address CA 123-334 United Statue;;;;;", + Arrays.asList("", "Formatted address CA 123-334 United Statue", + "", "", "", "", ""), new TypeSet("HOME")); + } + + public void testPostalOnlyWithFormattedAddressV21() { + testPostalOnlyWithFormattedAddressCommon(V21); + } + + public void testPostalOnlyWithFormattedAddressV30() { + testPostalOnlyWithFormattedAddressCommon(V30); + } + + /** + * Tests that the vCard composer honors formatted data when it is available + * even when it is partial. + */ + private void testPostalWithBothStructuredAndFormattedCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.POBOX, "Pobox") + .put(StructuredPostal.COUNTRY, "Country") + .put(StructuredPostal.FORMATTED_ADDRESS, + "Formatted address CA 123-334 United Statue"); // Should be ignored + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("ADR", "Pobox;;;;;;Country", + Arrays.asList("Pobox", "", "", "", "", "", "Country"), + new TypeSet("HOME")); + } + + public void testPostalWithBothStructuredAndFormattedV21() { + testPostalWithBothStructuredAndFormattedCommon(V21); + } + + public void testPostalWithBothStructuredAndFormattedV30() { + testPostalWithBothStructuredAndFormattedCommon(V30); + } + + private void testOrganizationCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "CompanyX") + .put(Organization.DEPARTMENT, "DepartmentY") + .put(Organization.TITLE, "TitleZ") + .put(Organization.JOB_DESCRIPTION, "Description Rambda") // Ignored. + .put(Organization.OFFICE_LOCATION, "Mountain View") // Ignored. + .put(Organization.PHONETIC_NAME, "PhoneticName!") // Ignored + .put(Organization.SYMBOL, "(^o^)/~~"); // Ignore him (her). + entry.addContentValues(Organization.CONTENT_ITEM_TYPE) + .putNull(Organization.COMPANY) + .put(Organization.DEPARTMENT, "DepartmentXX") + .putNull(Organization.TITLE); + entry.addContentValues(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "CompanyXYZ") + .putNull(Organization.DEPARTMENT) + .put(Organization.TITLE, "TitleXYZYX"); + // Currently we do not use group but depend on the order. + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNodeWithOrder("ORG", "CompanyX;DepartmentY", + Arrays.asList("CompanyX", "DepartmentY")) + .addExpectedNodeWithOrder("TITLE", "TitleZ") + .addExpectedNodeWithOrder("ORG", "DepartmentXX") + .addExpectedNodeWithOrder("ORG", "CompanyXYZ") + .addExpectedNodeWithOrder("TITLE", "TitleXYZYX"); + } + + public void testOrganizationV21() { + testOrganizationCommon(V21); + } + + public void testOrganizationV30() { + testOrganizationCommon(V30); + } + + private void testImVariousTypeSupportCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_AIM) + .put(Im.DATA, "aim"); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_MSN) + .put(Im.DATA, "msn"); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_YAHOO) + .put(Im.DATA, "yahoo"); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_SKYPE) + .put(Im.DATA, "skype"); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_QQ) + .put(Im.DATA, "qq"); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_GOOGLE_TALK) + .put(Im.DATA, "google talk"); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_ICQ) + .put(Im.DATA, "icq"); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_JABBER) + .put(Im.DATA, "jabber"); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_NETMEETING) + .put(Im.DATA, "netmeeting"); + + // No determined way to express unknown type... + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("X-JABBER", "jabber") + .addExpectedNode("X-ICQ", "icq") + .addExpectedNode("X-GOOGLE-TALK", "google talk") + .addExpectedNode("X-QQ", "qq") + .addExpectedNode("X-SKYPE-USERNAME", "skype") + .addExpectedNode("X-YAHOO", "yahoo") + .addExpectedNode("X-MSN", "msn") + .addExpectedNode("X-NETMEETING", "netmeeting") + .addExpectedNode("X-AIM", "aim"); + } + + public void testImBasiV21() { + testImVariousTypeSupportCommon(V21); + } + + public void testImBasicV30() { + testImVariousTypeSupportCommon(V30); + } + + private void testImPrefHandlingCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_AIM) + .put(Im.DATA, "aim1"); + entry.addContentValues(Im.CONTENT_ITEM_TYPE) + .put(Im.PROTOCOL, Im.PROTOCOL_AIM) + .put(Im.DATA, "aim2") + .put(Im.TYPE, Im.TYPE_HOME) + .put(Im.IS_PRIMARY, 1); + + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("X-AIM", "aim1") + .addExpectedNode("X-AIM", "aim2", new TypeSet("HOME", "PREF")); + } + + public void testImPrefHandlingV21() { + testImPrefHandlingCommon(V21); + } + + public void testImPrefHandlingV30() { + testImPrefHandlingCommon(V30); + } + + private void testWebsiteCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Website.CONTENT_ITEM_TYPE) + .put(Website.URL, "http://website.example.android.com/index.html") + .put(Website.TYPE, Website.TYPE_BLOG); + entry.addContentValues(Website.CONTENT_ITEM_TYPE) + .put(Website.URL, "ftp://ftp.example.android.com/index.html") + .put(Website.TYPE, Website.TYPE_FTP); + + // We drop TYPE information since vCard (especially 3.0) does not allow us to emit it. + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("URL", "ftp://ftp.example.android.com/index.html") + .addExpectedNode("URL", "http://website.example.android.com/index.html"); + } + + public void testWebsiteV21() { + testWebsiteCommon(V21); + } + + public void testWebsiteV30() { + testWebsiteCommon(V30); + } + + private String getAndroidPropValue(final String mimeType, String value, Integer type) { + return getAndroidPropValue(mimeType, value, type, null); + } + + private String getAndroidPropValue(final String mimeType, String value, + Integer type, String label) { + return (mimeType + ";" + value + ";" + + (type != null ? type : "") + ";" + + (label != null ? label : "") + ";;;;;;;;;;;;"); + } + + private void testEventCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Event.CONTENT_ITEM_TYPE) + .put(Event.TYPE, Event.TYPE_ANNIVERSARY) + .put(Event.START_DATE, "1982-06-16"); + entry.addContentValues(Event.CONTENT_ITEM_TYPE) + .put(Event.TYPE, Event.TYPE_BIRTHDAY) + .put(Event.START_DATE, "2008-10-22"); + entry.addContentValues(Event.CONTENT_ITEM_TYPE) + .put(Event.TYPE, Event.TYPE_OTHER) + .put(Event.START_DATE, "2018-03-12"); + entry.addContentValues(Event.CONTENT_ITEM_TYPE) + .put(Event.TYPE, Event.TYPE_CUSTOM) + .put(Event.LABEL, "The last day") + .put(Event.START_DATE, "When the Tower of Hanoi with 64 rings is completed."); + entry.addContentValues(Event.CONTENT_ITEM_TYPE) + .put(Event.TYPE, Event.TYPE_BIRTHDAY) + .put(Event.START_DATE, "2009-05-19"); // Should be ignored. + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("BDAY", "2008-10-22") + .addExpectedNode("X-ANDROID-CUSTOM", + getAndroidPropValue( + Event.CONTENT_ITEM_TYPE, "1982-06-16", Event.TYPE_ANNIVERSARY)) + .addExpectedNode("X-ANDROID-CUSTOM", + getAndroidPropValue( + Event.CONTENT_ITEM_TYPE, "2018-03-12", Event.TYPE_OTHER)) + .addExpectedNode("X-ANDROID-CUSTOM", + getAndroidPropValue( + Event.CONTENT_ITEM_TYPE, + "When the Tower of Hanoi with 64 rings is completed.", + Event.TYPE_CUSTOM, "The last day")); + } + + public void testEventV21() { + testEventCommon(V21); + } + + public void testEventV30() { + testEventCommon(V30); + } + + private void testNoteCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "note1"); + entry.addContentValues(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "note2") + .put(Note.IS_PRIMARY, 1); // Just ignored. + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNodeWithOrder("NOTE", "note1") + .addExpectedNodeWithOrder("NOTE", "note2"); + } + + public void testNoteV21() { + testNoteCommon(V21); + } + + public void testNoteV30() { + testNoteCommon(V30); + } + + private void testPhotoCommon(int vcardType) { + final boolean isV30 = vcardType == V30; + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "PhotoTest"); + entry.addContentValues(Photo.CONTENT_ITEM_TYPE) + .put(Photo.PHOTO, sPhotoByteArray); + + ContentValues contentValuesForPhoto = new ContentValues(); + contentValuesForPhoto.put("ENCODING", (isV30 ? "b" : "BASE64")); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNode("FN", "PhotoTest") + .addExpectedNode("N", "PhotoTest;;;;", + Arrays.asList("PhotoTest", "", "", "", "")) + .addExpectedNodeWithOrder("PHOTO", null, null, sPhotoByteArray, + contentValuesForPhoto, new TypeSet("JPEG"), null); + } + + public void testPhotoV21() { + testPhotoCommon(V21); + } + + public void testPhotoV30() { + testPhotoCommon(V30); + } + + private void testRelationCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + mVerifier.addInputEntry().addContentValues(Relation.CONTENT_ITEM_TYPE) + .put(Relation.TYPE, Relation.TYPE_MOTHER) + .put(Relation.NAME, "Ms. Mother"); + mVerifier.addContentValuesVerifierElem().addExpected(Relation.CONTENT_ITEM_TYPE) + .put(Relation.TYPE, Relation.TYPE_MOTHER) + .put(Relation.NAME, "Ms. Mother"); + } + + public void testRelationV21() { + testRelationCommon(V21); + } + + public void testRelationV30() { + testRelationCommon(V30); + } + + public void testV30HandleEscape() { + mVerifier.initForExportTest(V30); + mVerifier.addInputEntry().addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\\") + .put(StructuredName.GIVEN_NAME, ";") + .put(StructuredName.MIDDLE_NAME, ",") + .put(StructuredName.PREFIX, "\n") + .put(StructuredName.DISPLAY_NAME, "[<{Unescaped:Asciis}>]"); + // Verifies the vCard String correctly escapes each character which must be escaped. + mVerifier.addLineVerifierElem() + .addExpected("N:\\\\;\\;;\\,;\\n;") + .addExpected("FN:[<{Unescaped:Asciis}>]"); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNode("FN", "[<{Unescaped:Asciis}>]") + .addExpectedNode("N", Arrays.asList("\\", ";", ",", "\n", "")); + } + + /** + * There's no "NICKNAME" property in vCard 2.1, while there is in vCard 3.0. + * We use Android-specific "X-ANDROID-CUSTOM" property. + * This test verifies the functionality. + */ + public void testNickNameV21() { + mVerifier.initForExportTest(V21); + mVerifier.addInputEntry().addContentValues(Nickname.CONTENT_ITEM_TYPE) + .put(Nickname.NAME, "Nicky"); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("X-ANDROID-CUSTOM", + Nickname.CONTENT_ITEM_TYPE + ";Nicky;;;;;;;;;;;;;;"); + mVerifier.addContentValuesVerifierElem().addExpected(Nickname.CONTENT_ITEM_TYPE) + .put(Nickname.NAME, "Nicky"); + } + + public void testTolerateBrokenPhoneNumberEntryV21() { + mVerifier.initForExportTest(V21); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_HOME) + .put(Phone.NUMBER, "111-222-3333 (Miami)\n444-5555-666 (Tokyo);" + + "777-888-9999 (Chicago);111-222-3333 (Miami)"); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "111-222-3333", new TypeSet("HOME")) + .addExpectedNode("TEL", "444-555-5666", new TypeSet("HOME")) + .addExpectedNode("TEL", "777-888-9999", new TypeSet("HOME")); + } + + private void testPickUpNonEmptyContentValuesCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.IS_PRIMARY, 1); // Empty name. Should be ignored. + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "family1"); // Not primary. Should be ignored. + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.IS_PRIMARY, 1) + .put(StructuredName.FAMILY_NAME, "family2"); // This entry is what we want. + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.IS_PRIMARY, 1) + .put(StructuredName.FAMILY_NAME, "family3"); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "family4"); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNode("N", Arrays.asList("family2", "", "", "", "")) + .addExpectedNode("FN", "family2"); + } + + public void testPickUpNonEmptyContentValuesV21() { + testPickUpNonEmptyContentValuesCommon(V21); + } + + public void testPickUpNonEmptyContentValuesV30() { + testPickUpNonEmptyContentValuesCommon(V30); + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/VCardImporterTests.java b/vcard/tests/src/com/android/vcard/tests/VCardImporterTests.java new file mode 100644 index 000000000..045c0d94e --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/VCardImporterTests.java @@ -0,0 +1,1008 @@ +/* + * 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.tests; + +import android.content.ContentValues; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +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 com.android.vcard.VCardConfig; +import com.android.vcard.tests.test_utils.ContentValuesVerifier; +import com.android.vcard.tests.test_utils.ContentValuesVerifierElem; +import com.android.vcard.tests.test_utils.PropertyNodesVerifierElem.TypeSet; + +import java.util.Arrays; + +public class VCardImporterTests extends VCardTestsBase { + // Push data into int array at first since values like 0x80 are + // interpreted as int by the compiler and casting all of them is + // cumbersome... + private static final int[] sPhotoIntArrayForComplicatedCase = { + 0xff, 0xd8, 0xff, 0xe1, 0x0a, 0x0f, 0x45, 0x78, 0x69, 0x66, 0x00, + 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0d, + 0x01, 0x0e, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, + 0xaa, 0x01, 0x0f, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x00, 0xba, 0x01, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0xc2, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0xc8, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0xd0, 0x01, 0x28, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x01, 0x31, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xd8, 0x01, 0x32, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0xe6, 0x02, 0x13, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x82, + 0x98, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xfa, + 0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, + 0x84, 0xc4, 0xa5, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, + 0x01, 0x08, 0x00, 0x00, 0x04, 0x1e, 0x32, 0x30, 0x30, 0x38, 0x31, + 0x30, 0x32, 0x39, 0x31, 0x33, 0x35, 0x35, 0x33, 0x31, 0x00, 0x00, + 0x44, 0x6f, 0x43, 0x6f, 0x4d, 0x6f, 0x00, 0x00, 0x44, 0x39, 0x30, + 0x35, 0x69, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x44, 0x39, 0x30, + 0x35, 0x69, 0x20, 0x56, 0x65, 0x72, 0x31, 0x2e, 0x30, 0x30, 0x00, + 0x32, 0x30, 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, + 0x31, 0x33, 0x3a, 0x35, 0x35, 0x3a, 0x34, 0x37, 0x00, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x00, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x49, 0x4d, 0x00, 0x30, 0x33, + 0x30, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x14, 0x00, + 0x14, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x34, 0x01, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x10, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x11, 0x09, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x0f, 0x0b, 0x00, + 0x00, 0x27, 0x10, 0x00, 0x00, 0x05, 0x97, 0x00, 0x00, 0x27, 0x10, + 0x00, 0x00, 0x08, 0xb0, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x1c, + 0x01, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x02, 0x5e, 0x00, 0x00, + 0x27, 0x10, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x27, 0x10, 0x00, + 0x00, 0x03, 0xcb, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x1b, 0xe5, + 0x00, 0x00, 0x27, 0x10, 0x00, 0x28, 0x82, 0x9a, 0x00, 0x05, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x6a, 0x82, 0x9d, 0x00, 0x05, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x72, 0x88, 0x22, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x90, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, 0x32, 0x32, 0x30, 0x90, + 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x03, 0x7a, + 0x90, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x03, + 0x8e, 0x91, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, + 0x03, 0x00, 0x91, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x03, 0xa2, 0x92, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x03, 0xaa, 0x92, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x03, 0xb2, 0x92, 0x04, 0x00, 0x0a, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x03, 0xba, 0x92, 0x05, 0x00, 0x05, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xc2, 0x92, 0x07, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x92, 0x08, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x92, 0x09, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x92, + 0x0a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xca, + 0x92, 0x7c, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x92, 0x86, 0x00, 0x07, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, + 0x03, 0xd2, 0xa0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, + 0x31, 0x30, 0x30, 0xa0, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x60, 0x00, 0x00, 0xa0, 0x03, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x48, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x04, 0x00, 0xa2, 0x0e, 0x00, 0x05, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xe8, 0xa2, 0x0f, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xf0, 0xa2, 0x10, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0xa2, + 0x17, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, + 0xa3, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, + 0x00, 0xa3, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, + 0x00, 0x00, 0xa4, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x04, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x03, 0xf8, 0xa4, 0x05, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x1d, 0x00, 0x00, 0xa4, 0x06, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x07, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x08, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, + 0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0xa4, 0x0a, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0xa4, 0x0c, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, + 0x00, 0x27, 0x10, 0x00, 0x00, 0x01, 0x5e, 0x00, 0x00, 0x00, 0x64, + 0x32, 0x30, 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, + 0x31, 0x33, 0x3a, 0x35, 0x35, 0x3a, 0x33, 0x31, 0x00, 0x32, 0x30, + 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, 0x31, 0x33, + 0x3a, 0x35, 0x35, 0x3a, 0x34, 0x37, 0x00, 0x00, 0x00, 0x29, 0x88, + 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x02, 0xb2, 0x00, 0x00, 0x00, + 0x64, 0x00, 0x00, 0x01, 0x5e, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x25, 0x00, + 0x00, 0x00, 0x0a, 0x00, 0x00, 0x0e, 0x92, 0x00, 0x00, 0x03, 0xe8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x30, 0x30, + 0x38, 0x31, 0x30, 0x32, 0x39, 0x31, 0x33, 0x35, 0x35, 0x33, 0x31, + 0x00, 0x00, 0x20, 0x2a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x2a, + 0xe2, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x04, 0x52, 0x39, 0x38, 0x00, 0x00, 0x02, 0x00, 0x07, 0x00, 0x00, + 0x00, 0x04, 0x30, 0x31, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, + 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x04, 0x6c, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x04, 0x74, 0x01, 0x28, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x04, 0x7c, 0x02, 0x02, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0x8b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x01, 0xff, 0xd8, 0xff, 0xdb, 0x00, 0x84, + 0x00, 0x20, 0x16, 0x18, 0x1c, 0x18, 0x14, 0x20, 0x1c, 0x1a, 0x1c, + 0x24, 0x22, 0x20, 0x26, 0x30, 0x50, 0x34, 0x30, 0x2c, 0x2c, 0x30, + 0x62, 0x46, 0x4a, 0x3a, 0x50, 0x74, 0x66, 0x7a, 0x78, 0x72, 0x66, + 0x70, 0x6e, 0x80, 0x90, 0xb8, 0x9c, 0x80, 0x88, 0xae, 0x8a, 0x6e, + 0x70, 0xa0, 0xda, 0xa2, 0xae, 0xbe, 0xc4, 0xce, 0xd0, 0xce, 0x7c, + 0x9a, 0xe2, 0xf2, 0xe0, 0xc8, 0xf0, 0xb8, 0xca, 0xce, 0xc6, 0x01, + 0x22, 0x24, 0x24, 0x30, 0x2a, 0x30, 0x5e, 0x34, 0x34, 0x5e, 0xc6, + 0x84, 0x70, 0x84, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, + 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, + 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, + 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, + 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xff, 0xc0, + 0x00, 0x11, 0x08, 0x00, 0x78, 0x00, 0xa0, 0x03, 0x01, 0x21, 0x00, + 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x01, 0xa2, 0x00, + 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, + 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, + 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, + 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, + 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, + 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, + 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, + 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, + 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, + 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, + 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, + 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, + 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0x01, 0x00, + 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, + 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, + 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, + 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, + 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, + 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, + 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, + 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, + 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, + 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, + 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, + 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, + 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, + 0x14, 0x54, 0xaa, 0x2a, 0x46, 0x48, 0xa2, 0xa4, 0x55, 0xa6, 0x04, + 0x8a, 0x29, 0xe0, 0x53, 0x10, 0xe0, 0x29, 0xc0, 0x50, 0x03, 0xb1, + 0x46, 0x29, 0x80, 0x84, 0x52, 0x11, 0x40, 0x0d, 0x22, 0x9a, 0x45, + 0x20, 0x23, 0x61, 0x51, 0x30, 0xa0, 0x08, 0xc8, 0xa8, 0xd8, 0x52, + 0x02, 0x26, 0x15, 0x0b, 0x0a, 0x00, 0xb4, 0xa2, 0xa5, 0x5a, 0x00, + 0x91, 0x45, 0x4a, 0xa2, 0x81, 0x92, 0x01, 0x4e, 0x02, 0x98, 0x87, + 0x0a, 0x70, 0xa0, 0x07, 0x62, 0x8c, 0x50, 0x21, 0x0d, 0x25, 0x00, + 0x34, 0x8a, 0x61, 0x14, 0x0c, 0x63, 0x0a, 0x89, 0x85, 0x00, 0x46, + 0xd5, 0x1b, 0x52, 0x02, 0x16, 0xa8, 0x98, 0x50, 0x05, 0x94, 0xa9, + 0x16, 0x80, 0x25, 0x5a, 0x95, 0x68, 0x18, 0xf1, 0x4f, 0x14, 0xc4, + 0x3b, 0xb5, 0x22, 0xb6, 0x38, 0x34, 0x00, 0xe3, 0x22, 0x8e, 0xf4, + 0x79, 0x8a, 0x7b, 0xd1, 0x71, 0x03, 0x30, 0xc7, 0x14, 0x83, 0xa5, + 0x00, 0x06, 0x98, 0x68, 0x01, 0x8d, 0x51, 0x35, 0x03, 0x22, 0x6a, + 0x8d, 0xa9, 0x01, 0x13, 0x54, 0x4d, 0x40, 0x13, 0xa5, 0x4a, 0x28, + 0x02, 0x45, 0x35, 0x2a, 0x9a, 0x00, 0x78, 0x34, 0xf0, 0x69, 0x80, + 0x34, 0x81, 0x45, 0x40, 0xce, 0x58, 0xe6, 0xa2, 0x4c, 0x06, 0xe4, + 0xfa, 0xd1, 0x93, 0x50, 0x21, 0xca, 0xe4, 0x55, 0x84, 0x90, 0x30, + 0xab, 0x8b, 0x18, 0xa6, 0x9a, 0x6a, 0xc4, 0x31, 0xaa, 0x26, 0xa0, + 0x64, 0x4d, 0x51, 0xb5, 0x20, 0x23, 0x6a, 0x89, 0xa8, 0x02, 0x44, + 0x35, 0x2a, 0x9a, 0x00, 0x95, 0x4d, 0x48, 0xa6, 0x80, 0x24, 0x53, + 0x4e, 0xce, 0x05, 0x30, 0x2b, 0x3b, 0xee, 0x6a, 0x91, 0x5d, 0x76, + 0x63, 0xbd, 0x65, 0x7d, 0x40, 0x66, 0x68, 0xa9, 0x02, 0x45, 0x2b, + 0xb3, 0x9e, 0xb4, 0xc5, 0x6d, 0xad, 0x9a, 0xa0, 0x2c, 0x06, 0xc8, + 0xcd, 0x04, 0xd6, 0xa2, 0x23, 0x63, 0x51, 0xb1, 0xa0, 0x64, 0x4d, + 0x51, 0x93, 0x48, 0x08, 0xda, 0xa2, 0x6a, 0x00, 0x72, 0x1a, 0x99, + 0x4d, 0x00, 0x48, 0xa6, 0xa4, 0x53, 0x4c, 0x07, 0x86, 0x03, 0xbd, + 0x2b, 0x9c, 0xa7, 0x14, 0x98, 0x10, 0x85, 0x34, 0xe0, 0xa6, 0xb3, + 0xb0, 0x0b, 0xb5, 0xa8, 0x0a, 0xd4, 0x58, 0x42, 0xed, 0x3e, 0x94, + 0xd2, 0xa6, 0x8b, 0x01, 0x34, 0x44, 0xed, 0xe6, 0x9c, 0x4d, 0x6a, + 0x80, 0x8d, 0x8d, 0x46, 0xc6, 0x80, 0x23, 0x63, 0x51, 0x9a, 0x06, + 0x46, 0xd5, 0x13, 0x52, 0x01, 0x54, 0xd4, 0xaa, 0x68, 0x02, 0x40, + 0x6a, 0x40, 0x78, 0xa0, 0x08, 0x59, 0xce, 0xee, 0xb5, 0x2a, 0x39, + 0xd9, 0x59, 0xa7, 0xa8, 0x00, 0x73, 0xeb, 0x4e, 0x0e, 0x7d, 0x69, + 0x5c, 0x05, 0xf3, 0x0f, 0xad, 0x1e, 0x61, 0xf5, 0xa7, 0x71, 0x0b, + 0xe6, 0x35, 0x21, 0x90, 0xd3, 0xb8, 0x0e, 0x32, 0x10, 0x95, 0x10, + 0x91, 0xb3, 0xd6, 0x9b, 0x60, 0x4b, 0x9c, 0x8a, 0x63, 0x1a, 0xb0, + 0x18, 0x4d, 0x46, 0xc6, 0x80, 0x22, 0x6a, 0x61, 0xa4, 0x31, 0xaa, + 0x6a, 0x55, 0x34, 0x01, 0x2a, 0x9a, 0x7e, 0x78, 0xa0, 0x08, 0x09, + 0xf9, 0xaa, 0x58, 0xcf, 0xca, 0x6b, 0x3e, 0xa0, 0x00, 0xd3, 0x81, + 0xa9, 0x01, 0x73, 0x46, 0x69, 0x80, 0xb9, 0xa4, 0xcd, 0x00, 0x2b, + 0x1f, 0x92, 0xa3, 0x07, 0x9a, 0x6f, 0x70, 0x26, 0xcf, 0x14, 0xd2, + 0x6b, 0x51, 0x0c, 0x63, 0x51, 0xb1, 0xa0, 0x08, 0xda, 0x98, 0x69, + 0x0c, 0x8d, 0x4d, 0x4a, 0xa6, 0x80, 0x24, 0x53, 0x52, 0x03, 0xc5, + 0x02, 0x21, 0x27, 0xe6, 0xa9, 0x23, 0x3f, 0x29, 0xac, 0xfa, 0x8c, + 0x01, 0xe6, 0x9c, 0x0d, 0x48, 0x0a, 0x0d, 0x2e, 0x68, 0x01, 0x73, + 0x49, 0x9a, 0x60, 0x2b, 0x1f, 0x92, 0x98, 0x3a, 0xd3, 0x7b, 0x81, + 0x36, 0x78, 0xa6, 0x93, 0x5a, 0x88, 0x8c, 0x9a, 0x63, 0x1a, 0x00, + 0x8c, 0xd3, 0x0d, 0x21, 0x91, 0x29, 0xa9, 0x14, 0xd0, 0x04, 0x8a, + 0x69, 0xe0, 0xd3, 0x11, 0x1b, 0x1e, 0x6a, 0x48, 0xcf, 0xca, 0x6b, + 0x3e, 0xa3, 0x10, 0x1a, 0x70, 0x35, 0x20, 0x38, 0x1a, 0x5c, 0xd2, + 0x01, 0x73, 0x49, 0x9a, 0x60, 0x39, 0x8f, 0xca, 0x29, 0x8b, 0xf7, + 0xaa, 0xba, 0x88, 0x96, 0x9a, 0x6b, 0x40, 0x18, 0xc6, 0xa3, 0x26, + 0x80, 0x18, 0x69, 0xa6, 0x90, 0xc8, 0x14, 0xd4, 0x8a, 0x69, 0x80, + 0xf0, 0x6a, 0x40, 0x68, 0x10, 0xbb, 0x41, 0xa7, 0xe3, 0x0b, 0xc5, + 0x2b, 0x01, 0x10, 0xa7, 0x03, 0x59, 0x0c, 0x76, 0x69, 0x73, 0x40, + 0x0b, 0x9a, 0x28, 0x11, 0x28, 0x19, 0x5e, 0x69, 0x02, 0x81, 0x5a, + 0xd8, 0x00, 0xd3, 0x4d, 0x50, 0x0c, 0x6a, 0x8c, 0xd2, 0x01, 0xa6, + 0x98, 0x69, 0x0c, 0xae, 0xa6, 0xa4, 0x06, 0x80, 0x1e, 0xa6, 0x9e, + 0x0d, 0x31, 0x12, 0x03, 0x4f, 0x06, 0x80, 0x13, 0x60, 0x34, 0xd3, + 0xc1, 0xa8, 0x92, 0x01, 0xf1, 0x8d, 0xdd, 0x69, 0xcc, 0xa1, 0x69, + 0x5b, 0x4b, 0x80, 0x83, 0x93, 0x52, 0x04, 0x14, 0xe2, 0xae, 0x03, + 0xa9, 0x0d, 0x68, 0x03, 0x4d, 0x34, 0xd0, 0x03, 0x0d, 0x30, 0xd2, + 0x01, 0x86, 0x9a, 0x68, 0x19, 0x58, 0x1a, 0x78, 0xa4, 0x04, 0x8a, + 0x69, 0xe0, 0xd3, 0x10, 0xe0, 0x69, 0xe0, 0xd0, 0x03, 0xc1, 0xa8, + 0xdb, 0xad, 0x4c, 0x81, 0x12, 0x45, 0xd6, 0x9d, 0x25, 0x1d, 0x00, + 0x6a, 0xf5, 0xa9, 0xe8, 0x80, 0x31, 0x29, 0x0d, 0x58, 0x08, 0x69, + 0x86, 0x80, 0x1a, 0x69, 0x86, 0x90, 0x0c, 0x34, 0xd3, 0x48, 0x65, + 0x51, 0x4f, 0x06, 0x98, 0x0f, 0x14, 0xf0, 0x68, 0x10, 0xf0, 0x69, + 0xe0, 0xd0, 0x03, 0x81, 0xa5, 0x2b, 0x9a, 0x1a, 0xb8, 0x87, 0xa8, + 0xdb, 0x4a, 0x46, 0x68, 0xb6, 0x80, 0x2a, 0xa8, 0x14, 0xea, 0x12, + 0xb0, 0x05, 0x21, 0xa6, 0x02, 0x1a, 0x61, 0xa0, 0x06, 0x9a, 0x61, + 0xa4, 0x31, 0x86, 0x9a, 0x69, 0x0c, 0xa8, 0x0d, 0x3c, 0x53, 0x01, + 0xe2, 0x9e, 0x28, 0x10, 0xf1, 0x4e, 0x06, 0x98, 0x0f, 0x06, 0x9e, + 0x0d, 0x02, 0x1c, 0x29, 0xc2, 0x80, 0x16, 0x96, 0x80, 0x0a, 0x4a, + 0x00, 0x43, 0x4d, 0x34, 0x0c, 0x61, 0xa6, 0x1a, 0x40, 0x34, 0xd3, + 0x4d, 0x21, 0x80, 0xff, 0xd9, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x0a, + 0x07, 0x07, 0x08, 0x07, 0x06, 0x0a, 0x08, 0x08, 0x08, 0x0b, 0x0a, + 0x0a, 0x0b, 0x0e, 0x18, 0x10, 0x0e, 0x0d, 0x0d, 0x0e, 0x1d, 0x15, + 0x16, 0x11, 0x18, 0x23, 0x1f, 0x25, 0x24, 0x22, 0x1f, 0x22, 0x21, + 0x26, 0x2b, 0x37, 0x2f, 0x26, 0x29, 0x34, 0x29, 0x21, 0x22, 0x30, + 0x41, 0x31, 0x34, 0x39, 0x3b, 0x3e, 0x3e, 0x3e, 0x25, 0x2e, 0x44, + 0x49, 0x43, 0x3c, 0x48, 0x37, 0x3d, 0x3e, 0x3b, 0x01, 0x0a, 0x0b, + 0x0b, 0x0e, 0x0d, 0x0e, 0x1c, 0x10, 0x10, 0x1c, 0x3b, 0x28, 0x22, + 0x28, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0xff, 0xc0, 0x00, 0x11, + 0x08, 0x00, 0x48, 0x00, 0x60, 0x03, 0x01, 0x21, 0x00, 0x02, 0x11, + 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x01, 0xa2, 0x00, 0x00, 0x01, + 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, + 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, + 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, + 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, + 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, + 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, + 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, + 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, + 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, + 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, + 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0x01, 0x00, 0x03, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, + 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, + 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, + 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, + 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, + 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, + 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, + 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, + 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, + 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, + 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, + 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, + 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, + 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0x9e, 0xd2, + 0x2e, 0x07, 0x15, 0xaf, 0x6d, 0x08, 0xe2, 0xb3, 0x45, 0x1a, 0xf6, + 0xd0, 0x00, 0x01, 0xc5, 0x68, 0x45, 0x17, 0x4a, 0xb4, 0x22, 0xe4, + 0x70, 0x8c, 0x74, 0xa9, 0x3c, 0xa1, 0x8e, 0x95, 0x48, 0x96, 0x31, + 0xe2, 0x18, 0xe9, 0x55, 0xa5, 0x8c, 0x7a, 0x50, 0x05, 0x0b, 0x88, + 0x86, 0x0f, 0x15, 0x8f, 0x75, 0x1f, 0x26, 0x93, 0x19, 0x91, 0x77, + 0x18, 0xc1, 0xac, 0x4b, 0xc8, 0xfa, 0xd6, 0x63, 0x37, 0x6d, 0x31, + 0xb4, 0x73, 0x5b, 0x36, 0xa0, 0x1c, 0x50, 0x80, 0xd7, 0x83, 0xa0, + 0xab, 0xd1, 0x62, 0xad, 0x09, 0x8f, 0x17, 0x29, 0x03, 0xb2, 0xcc, + 0xe0, 0x77, 0x14, 0xa3, 0x56, 0xb3, 0x27, 0x1e, 0x67, 0xe9, 0x52, + 0xea, 0xc6, 0x3a, 0x36, 0x48, 0xef, 0x3d, 0x27, 0x70, 0x22, 0x60, + 0x47, 0x52, 0x69, 0xb2, 0xe2, 0xad, 0x3b, 0xea, 0x80, 0xa3, 0x38, + 0xe0, 0xd6, 0x3d, 0xd8, 0x1c, 0xd0, 0xca, 0x46, 0x3d, 0xd0, 0x18, + 0x35, 0x89, 0x78, 0xa3, 0x9a, 0xcd, 0x8c, 0xd2, 0xb3, 0x93, 0x2a, + 0x2b, 0x66, 0xd5, 0xf1, 0x8a, 0x10, 0x1a, 0xd6, 0xf2, 0x03, 0x8a, + 0x9e, 0xe6, 0xf4, 0x5a, 0xdb, 0xef, 0xfe, 0x23, 0xc0, 0xa7, 0x27, + 0xcb, 0x16, 0xc4, 0xcc, 0xdd, 0xe2, 0x78, 0x9a, 0x69, 0x66, 0xcc, + 0x99, 0xe1, 0x4d, 0x47, 0xba, 0xbc, 0xd9, 0x6a, 0xee, 0x26, 0x59, + 0x59, 0x4d, 0xac, 0x69, 0x34, 0x52, 0xe5, 0x8f, 0x55, 0xad, 0x58, + 0xae, 0x85, 0xc4, 0x22, 0x41, 0xdf, 0xad, 0x76, 0x61, 0xe5, 0x6f, + 0x74, 0x45, 0x69, 0xdc, 0x00, 0x79, 0xac, 0x8b, 0xa6, 0xc9, 0x35, + 0xd4, 0x34, 0x64, 0xdc, 0x37, 0x06, 0xb1, 0xae, 0x88, 0xc1, 0xac, + 0xd8, 0xc9, 0x2c, 0xa6, 0xe0, 0x73, 0x5b, 0x36, 0xf3, 0x74, 0xe6, + 0x84, 0x05, 0xe3, 0xa9, 0x47, 0x6a, 0x14, 0xb6, 0x49, 0x3d, 0x85, + 0x3a, 0xee, 0xee, 0x2b, 0xa8, 0xe2, 0x6f, 0x30, 0x81, 0xe9, 0x8a, + 0xca, 0xa4, 0xe2, 0xd3, 0x8b, 0x01, 0xb1, 0xf9, 0x04, 0x7f, 0xaf, + 0x23, 0xf0, 0xa9, 0x54, 0x41, 0x9c, 0xfd, 0xa3, 0xf4, 0xae, 0x65, + 0x18, 0xf7, 0x25, 0x8a, 0xe2, 0x02, 0x38, 0xb8, 0xfd, 0x2a, 0x7b, + 0x5b, 0xa8, 0x6d, 0x6d, 0x5d, 0x9a, 0x5d, 0xcb, 0xbb, 0xd2, 0xb6, + 0xa6, 0xa3, 0x19, 0x5e, 0xe2, 0x03, 0x7b, 0x1d, 0xc2, 0x17, 0x8d, + 0xb8, 0xac, 0xfb, 0x89, 0x39, 0x35, 0xd6, 0x9a, 0x6a, 0xe8, 0x66, + 0x55, 0xcb, 0xf5, 0xac, 0x7b, 0x96, 0xeb, 0x50, 0xc6, 0x88, 0x6d, + 0x66, 0xe9, 0xcd, 0x6c, 0xdb, 0x4f, 0xd3, 0x9a, 0x00, 0x2f, 0xe6, + 0xf9, 0xa3, 0xe7, 0xb5, 0x4a, 0x93, 0x7f, 0xa2, 0xc6, 0x73, 0xdc, + 0xd7, 0x15, 0x55, 0xef, 0x48, 0x7d, 0x09, 0x52, 0x6e, 0x3a, 0xd4, + 0xab, 0x2f, 0xbd, 0x61, 0x16, 0x0c, 0x73, 0x49, 0xc5, 0x24, 0x92, + 0x7f, 0xa2, 0x63, 0xfd, 0xaa, 0xd6, 0x2f, 0x71, 0x0e, 0xb1, 0x93, + 0xf7, 0x2d, 0xf5, 0xa4, 0x9e, 0x4e, 0xb5, 0xdd, 0x4b, 0xf8, 0x68, + 0x4c, 0xcb, 0xb9, 0x93, 0xad, 0x65, 0xce, 0xd9, 0x26, 0xa9, 0x8d, + 0x19, 0xf6, 0xf2, 0xf4, 0xe6, 0xb5, 0xad, 0xe7, 0xc6, 0x39, 0xa0, + 0x18, 0xeb, 0xc9, 0x77, 0x6c, 0x35, 0x2a, 0x4b, 0xfe, 0x8a, 0x9c, + 0xff, 0x00, 0x11, 0xae, 0x3a, 0x8b, 0xde, 0x61, 0xd0, 0x9e, 0x39, + 0xb8, 0xeb, 0x53, 0xac, 0xb9, 0xae, 0x5b, 0x00, 0xf3, 0x27, 0x14, + 0x92, 0xc9, 0xfe, 0x8a, 0x3f, 0xde, 0x35, 0xac, 0x3a, 0x88, 0x92, + 0xcd, 0xb1, 0x6e, 0x7d, 0xcd, 0x32, 0x67, 0xeb, 0xcd, 0x7a, 0x14, + 0xfe, 0x04, 0x26, 0x66, 0xce, 0xf9, 0x26, 0xb3, 0xe6, 0x6e, 0xb4, + 0xd9, 0x48, 0xc8, 0x82, 0x4e, 0x07, 0x35, 0xa7, 0x6f, 0x2f, 0x02, + 0x9a, 0x06, 0x5f, 0x8c, 0xa4, 0x83, 0x0e, 0x32, 0x2a, 0x69, 0xe3, + 0xdd, 0x12, 0x08, 0x97, 0x85, 0xec, 0x2a, 0x2a, 0x42, 0xf1, 0x76, + 0x26, 0xe4, 0x6a, 0x59, 0x0e, 0x18, 0x10, 0x6a, 0xd2, 0x89, 0x02, + 0x6e, 0x2a, 0x71, 0xeb, 0x5c, 0x1c, 0x8c, 0xa6, 0x48, 0xbb, 0xdc, + 0x61, 0x41, 0x35, 0x72, 0x28, 0x87, 0xd9, 0xf6, 0x4a, 0xb9, 0xe7, + 0x38, 0xae, 0x8c, 0x3d, 0x36, 0xdd, 0xde, 0xc4, 0xb0, 0x21, 0x51, + 0x76, 0xa8, 0xc0, 0xaa, 0x93, 0x31, 0xe6, 0xbb, 0x2d, 0x65, 0x61, + 0x19, 0xd3, 0x1e, 0xb5, 0x46, 0x5a, 0x96, 0x5a, 0x30, 0xa0, 0x7e, + 0x05, 0x69, 0x5b, 0xc9, 0xc6, 0x28, 0x40, 0xcd, 0x08, 0x64, 0x3c, + 0x73, 0x57, 0xe1, 0x94, 0xf1, 0xcd, 0x5a, 0x21, 0x8c, 0xb9, 0x63, + 0xe7, 0x67, 0x1d, 0xab, 0x40, 0xb1, 0xfb, 0x00, 0x1d, 0xf0, 0x2b, + 0x99, 0x2d, 0x66, 0x3e, 0x88, 0x75, 0x81, 0x3f, 0x31, 0xf6, 0xab, + 0x64, 0xd6, 0xb4, 0x17, 0xee, 0xd0, 0x9e, 0xe4, 0x32, 0x1a, 0xa7, + 0x31, 0xad, 0x18, 0x14, 0x26, 0xef, 0x54, 0xa5, 0xa8, 0x65, 0xa3, + 0x9c, 0x81, 0xfa, 0x56, 0x8c, 0x2d, 0xce, 0x68, 0x40, 0xcb, 0xf1, + 0x37, 0xbd, 0x5e, 0x85, 0xea, 0xd1, 0x0c, 0xbb, 0x19, 0x56, 0x23, + 0x20, 0x1f, 0xad, 0x5c, 0x42, 0x08, 0x03, 0xb5, 0x55, 0x91, 0x04, + 0xc9, 0x80, 0x38, 0x00, 0x0a, 0x71, 0x34, 0x6c, 0x32, 0x27, 0xe9, + 0x55, 0x25, 0x15, 0x2c, 0x68, 0xa3, 0x30, 0xeb, 0x54, 0xa5, 0x15, + 0x0c, 0xd1, 0x00, 0xff, 0xd9}; + + /* package */ static final byte[] sPhotoByteArrayForComplicatedCase; + + static { + final int length = sPhotoIntArrayForComplicatedCase.length; + sPhotoByteArrayForComplicatedCase = new byte[length]; + for (int i = 0; i < length; i++) { + sPhotoByteArrayForComplicatedCase[i] = (byte)sPhotoIntArrayForComplicatedCase[i]; + } + } + + public void testV21SimpleCase1_Parsing() { + mVerifier.initForImportTest(V21, R.raw.v21_simple_1); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("N", "Ando;Roid;", Arrays.asList("Ando", "Roid", "")); + } + + public void testV21SimpleCase1_Type_Generic() { + mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_GENERIC, R.raw.v21_simple_1); + mVerifier.addContentValuesVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Ando") + .put(StructuredName.GIVEN_NAME, "Roid") + .put(StructuredName.DISPLAY_NAME, "Roid Ando"); + } + + public void testV21SimpleCase1_Type_Japanese() { + mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_simple_1); + mVerifier.addContentValuesVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Ando") + .put(StructuredName.GIVEN_NAME, "Roid") + // If name-related strings only contains printable Ascii, + // the order is remained to be US's: + // "Prefix Given Middle Family Suffix" + .put(StructuredName.DISPLAY_NAME, "Roid Ando"); + } + + public void testV21SimpleCase2() { + mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_simple_2); + mVerifier.addContentValuesVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.DISPLAY_NAME, "Ando Roid"); + } + + public void testV21SimpleCase3() { + mVerifier.initForImportTest(V21, R.raw.v21_simple_3); + mVerifier.addContentValuesVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Ando") + .put(StructuredName.GIVEN_NAME, "Roid") + // "FN" field should be prefered since it should contain the original + // order intended by the author of the file. + .put(StructuredName.DISPLAY_NAME, "Ando Roid"); + } + + /** + * Tests ';' is properly handled by VCardParser implementation. + */ + public void testV21BackslashCase_Parsing() { + mVerifier.initForImportTest(V21, R.raw.v21_backslash); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("VERSION", "2.1") + .addExpectedNodeWithOrder("N", ";A;B\\;C\\;;D;:E;\\\\;", + Arrays.asList("", "A;B\\", "C\\;", "D", ":E", "\\\\", "")) + .addExpectedNodeWithOrder("FN", "A;B\\C\\;D:E\\\\"); + + } + + /** + * Tests ContactStruct correctly ignores redundant fields in "N" property values and + * inserts name related data. + */ + public void testV21BackslashCase() { + mVerifier.initForImportTest(V21, R.raw.v21_backslash); + mVerifier.addContentValuesVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE) + // FAMILY_NAME is empty and removed in this test... + .put(StructuredName.GIVEN_NAME, "A;B\\") + .put(StructuredName.MIDDLE_NAME, "C\\;") + .put(StructuredName.PREFIX, "D") + .put(StructuredName.SUFFIX, ":E") + .put(StructuredName.DISPLAY_NAME, "A;B\\C\\;D:E\\\\"); + } + + public void testOrgBeforTitle() { + mVerifier.initForImportTest(V21, R.raw.v21_org_before_title); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.DISPLAY_NAME, "Normal Guy"); + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Company") + .put(Organization.DEPARTMENT, "Organization Devision Room Sheet No.") + .put(Organization.TITLE, "Excellent Janitor") + .put(Organization.TYPE, Organization.TYPE_WORK); + } + + public void testTitleBeforOrg() { + mVerifier.initForImportTest(V21, R.raw.v21_title_before_org); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.DISPLAY_NAME, "Nice Guy"); + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Marverous") + .put(Organization.DEPARTMENT, "Perfect Great Good Bad Poor") + .put(Organization.TITLE, "Cool Title") + .put(Organization.TYPE, Organization.TYPE_WORK); + } + + /** + * Verifies that vCard importer correctly interpret "PREF" attribute to IS_PRIMARY. + * The data contain three cases: one "PREF", no "PREF" and multiple "PREF", in each type. + */ + public void testV21PrefToIsPrimary() { + mVerifier.initForImportTest(V21, R.raw.v21_pref_handling); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.DISPLAY_NAME, "Smith"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "1") + .put(Phone.TYPE, Phone.TYPE_HOME); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "2") + .put(Phone.TYPE, Phone.TYPE_WORK) + .put(Phone.IS_PRIMARY, 1); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "3") + .put(Phone.TYPE, Phone.TYPE_ISDN); + elem.addExpected(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "test@example.com") + .put(Email.TYPE, Email.TYPE_HOME) + .put(Email.IS_PRIMARY, 1); + elem.addExpected(Email.CONTENT_ITEM_TYPE) + .put(Email.DATA, "test2@examination.com") + .put(Email.TYPE, Email.TYPE_MOBILE) + .put(Email.IS_PRIMARY, 1); + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Company") + .put(Organization.TITLE, "Engineer") + .put(Organization.TYPE, Organization.TYPE_WORK); + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Mystery") + .put(Organization.TITLE, "Blogger") + .put(Organization.TYPE, Organization.TYPE_WORK); + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Poetry") + .put(Organization.TITLE, "Poet") + .put(Organization.TYPE, Organization.TYPE_WORK); + } + + /** + * Tests all the properties in a complicated vCard are correctly parsed by the VCardParser. + */ + public void testV21ComplicatedCase_Parsing() { + mVerifier.initForImportTest(V21, R.raw.v21_complicated); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("VERSION", "2.1") + .addExpectedNodeWithOrder("N", "Gump;Forrest;Hoge;Pos;Tao", + Arrays.asList("Gump", "Forrest", "Hoge", "Pos", "Tao")) + .addExpectedNodeWithOrder("FN", "Joe Due") + .addExpectedNodeWithOrder("ORG", "Gump Shrimp Co.;Sales Dept.;Manager;Fish keeper", + Arrays.asList("Gump Shrimp Co.", "Sales Dept.;Manager", "Fish keeper")) + .addExpectedNodeWithOrder("ROLE", "Fish Cake Keeper!") + .addExpectedNodeWithOrder("TITLE", "Shrimp Man") + .addExpectedNodeWithOrder("X-CLASS", "PUBLIC") + .addExpectedNodeWithOrder("TEL", "(111) 555-1212", new TypeSet("WORK", "VOICE")) + .addExpectedNodeWithOrder("TEL", "(404) 555-1212", new TypeSet("HOME", "VOICE")) + .addExpectedNodeWithOrder("TEL", "0311111111", new TypeSet("CELL")) + .addExpectedNodeWithOrder("TEL", "0322222222", new TypeSet("VIDEO")) + .addExpectedNodeWithOrder("TEL", "0333333333", new TypeSet("VOICE")) + .addExpectedNodeWithOrder("ADR", + ";;100 Waters Edge;Baytown;LA;30314;United States of America", + Arrays.asList("", "", "100 Waters Edge", "Baytown", + "LA", "30314", "United States of America"), + null, null, new TypeSet("WORK"), null) + .addExpectedNodeWithOrder("LABEL", + "100 Waters Edge\r\nBaytown, LA 30314\r\nUnited States of America", + null, null, mContentValuesForQP, new TypeSet("WORK"), null) + .addExpectedNodeWithOrder("ADR", + ";;42 Plantation St.;Baytown;LA;30314;United States of America", + Arrays.asList("", "", "42 Plantation St.", "Baytown", + "LA", "30314", "United States of America"), null, null, + new TypeSet("HOME"), null) + .addExpectedNodeWithOrder("LABEL", + "42 Plantation St.\r\nBaytown, LA 30314\r\nUnited States of America", + null, null, mContentValuesForQP, + new TypeSet("HOME"), null) + .addExpectedNodeWithOrder("EMAIL", "forrestgump@walladalla.com", + new TypeSet("PREF", "INTERNET")) + .addExpectedNodeWithOrder("EMAIL", "cell@example.com", new TypeSet("CELL")) + .addExpectedNodeWithOrder("NOTE", "The following note is the example from RFC 2045.") + .addExpectedNodeWithOrder("NOTE", + "Now's the time for all folk to come to the aid of their country.", + null, null, mContentValuesForQP, null, null) + .addExpectedNodeWithOrder("PHOTO", null, + null, sPhotoByteArrayForComplicatedCase, mContentValuesForBase64V21, + new TypeSet("JPEG"), null) + .addExpectedNodeWithOrder("X-ATTRIBUTE", "Some String") + .addExpectedNodeWithOrder("BDAY", "19800101") + .addExpectedNodeWithOrder("GEO", "35.6563854,139.6994233") + .addExpectedNodeWithOrder("URL", "http://www.example.com/") + .addExpectedNodeWithOrder("REV", "20080424T195243Z"); + } + + /** + * Checks ContactStruct correctly inserts values in a complicated vCard + * into ContentResolver. + */ + public void testV21ComplicatedCase() { + mVerifier.initForImportTest(V21, R.raw.v21_complicated); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Gump") + .put(StructuredName.GIVEN_NAME, "Forrest") + .put(StructuredName.MIDDLE_NAME, "Hoge") + .put(StructuredName.PREFIX, "Pos") + .put(StructuredName.SUFFIX, "Tao") + .put(StructuredName.DISPLAY_NAME, "Joe Due"); + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.TYPE, Organization.TYPE_WORK) + .put(Organization.COMPANY, "Gump Shrimp Co.") + .put(Organization.DEPARTMENT, "Sales Dept.;Manager Fish keeper") + .put(Organization.TITLE, "Shrimp Man"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_WORK) + // Phone number is expected to be formated with NAMP format in default. + .put(Phone.NUMBER, "111-555-1212"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_HOME) + .put(Phone.NUMBER, "404-555-1212"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_MOBILE) + .put(Phone.NUMBER, "031-111-1111"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "VIDEO") + .put(Phone.NUMBER, "032-222-2222"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "VOICE") + .put(Phone.NUMBER, "033-333-3333"); + elem.addExpected(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK) + .put(StructuredPostal.COUNTRY, "United States of America") + .put(StructuredPostal.POSTCODE, "30314") + .put(StructuredPostal.REGION, "LA") + .put(StructuredPostal.CITY, "Baytown") + .put(StructuredPostal.STREET, "100 Waters Edge") + .put(StructuredPostal.FORMATTED_ADDRESS, + "100 Waters Edge Baytown LA 30314 United States of America"); + elem.addExpected(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME) + .put(StructuredPostal.COUNTRY, "United States of America") + .put(StructuredPostal.POSTCODE, "30314") + .put(StructuredPostal.REGION, "LA") + .put(StructuredPostal.CITY, "Baytown") + .put(StructuredPostal.STREET, "42 Plantation St.") + .put(StructuredPostal.FORMATTED_ADDRESS, + "42 Plantation St. Baytown LA 30314 United States of America"); + elem.addExpected(Email.CONTENT_ITEM_TYPE) + // "TYPE=INTERNET" -> TYPE_CUSTOM + the label "INTERNET" + .put(Email.TYPE, Email.TYPE_CUSTOM) + .put(Email.LABEL, "INTERNET") + .put(Email.DATA, "forrestgump@walladalla.com") + .put(Email.IS_PRIMARY, 1); + elem.addExpected(Email.CONTENT_ITEM_TYPE) + .put(Email.TYPE, Email.TYPE_MOBILE) + .put(Email.DATA, "cell@example.com"); + elem.addExpected(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "The following note is the example from RFC 2045."); + elem.addExpected(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, + "Now's the time for all folk to come to the aid of their country."); + elem.addExpected(Photo.CONTENT_ITEM_TYPE) + // No information about its image format can be inserted. + .put(Photo.PHOTO, sPhotoByteArrayForComplicatedCase); + elem.addExpected(Event.CONTENT_ITEM_TYPE) + .put(Event.START_DATE, "19800101") + .put(Event.TYPE, Event.TYPE_BIRTHDAY); + elem.addExpected(Website.CONTENT_ITEM_TYPE) + .put(Website.URL, "http://www.example.com/") + .put(Website.TYPE, Website.TYPE_HOMEPAGE); + } + + public void testV30Simple_Parsing() { + mVerifier.initForImportTest(V30, R.raw.v30_simple); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("VERSION", "3.0") + .addExpectedNodeWithOrder("FN", "And Roid") + .addExpectedNodeWithOrder("N", "And;Roid;;;", Arrays.asList("And", "Roid", "", "", "")) + .addExpectedNodeWithOrder("ORG", "Open;Handset; Alliance", + Arrays.asList("Open", "Handset", " Alliance")) + .addExpectedNodeWithOrder("SORT-STRING", "android") + .addExpectedNodeWithOrder("TEL", "0300000000", new TypeSet("PREF", "VOICE")) + .addExpectedNodeWithOrder("CLASS", "PUBLIC") + .addExpectedNodeWithOrder("X-GNO", "0") + .addExpectedNodeWithOrder("X-GN", "group0") + .addExpectedNodeWithOrder("X-REDUCTION", "0") + .addExpectedNodeWithOrder("REV", "20081031T065854Z"); + } + + public void testV30Simple() { + mVerifier.initForImportTest(V30, R.raw.v30_simple); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "And") + .put(StructuredName.GIVEN_NAME, "Roid") + .put(StructuredName.DISPLAY_NAME, "And Roid") + .put(StructuredName.PHONETIC_GIVEN_NAME, "android"); + elem.addExpected(Organization.CONTENT_ITEM_TYPE) + .put(Organization.COMPANY, "Open") + .put(Organization.DEPARTMENT, "Handset Alliance") + .put(Organization.TYPE, Organization.TYPE_WORK); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "VOICE") + .put(Phone.NUMBER, "030-000-0000") + .put(Phone.IS_PRIMARY, 1); + } + + public void testV21Japanese1_Parsing() { + // Though Japanese careers append ";;;;" at the end of the value of "SOUND", + // vCard 2.1/3.0 specification does not allow multiple values. + // Do not need to handle it as multiple values. + mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_japanese_1); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("VERSION", "2.1", null, null, null, null, null) + .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9;;;;", + Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9", "", "", "", ""), + null, mContentValuesForSJis, null, null) + .addExpectedNodeWithOrder("SOUND", + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E;;;;", + null, null, mContentValuesForSJis, + new TypeSet("X-IRMC-N"), null) + .addExpectedNodeWithOrder("TEL", "0300000000", null, null, null, + new TypeSet("VOICE", "PREF"), null); + } + + private void testV21Japanese1Common(int resId, int vcardType, boolean japanese) { + mVerifier.initForImportTest(vcardType, resId); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9") + .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9") + // While vCard parser does not split "SOUND" property values, + // ContactStruct care it. + .put(StructuredName.PHONETIC_GIVEN_NAME, + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + // Phone number formatting is different. + .put(Phone.NUMBER, (japanese ? "03-0000-0000" : "030-000-0000")) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "VOICE") + .put(Phone.IS_PRIMARY, 1); + } + + /** + * Verifies vCard with Japanese can be parsed correctly with + * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_GENERIC}. + */ + public void testV21Japanese1_Type_Generic_Utf8() { + testV21Japanese1Common( + R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_GENERIC, false); + } + + /** + * Verifies vCard with Japanese can be parsed correctly with + * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_JAPANESE}. + */ + public void testV21Japanese1_Type_Japanese_Sjis() { + testV21Japanese1Common( + R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE, true); + } + + /** + * Verifies vCard with Japanese can be parsed correctly with + * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_JAPANESE}. + * since vCard 2.1 specifies the charset of each line if it contains non-Ascii. + */ + public void testV21Japanese1_Type_Japanese_Utf8() { + testV21Japanese1Common( + R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE, true); + } + + public void testV21Japanese2_Parsing() { + mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_japanese_2); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("VERSION", "2.1") + .addExpectedNodeWithOrder("N", "\u5B89\u85E4;\u30ED\u30A4\u30C9\u0031;;;", + Arrays.asList("\u5B89\u85E4", "\u30ED\u30A4\u30C9\u0031", + "", "", ""), + null, mContentValuesForSJis, null, null) + .addExpectedNodeWithOrder("FN", "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9\u0020\u0031", + null, null, mContentValuesForSJis, null, null) + .addExpectedNodeWithOrder("SOUND", + "\uFF71\uFF9D\uFF84\uFF9E\uFF73;\uFF9B\uFF72\uFF84\uFF9E\u0031;;;", + null, null, mContentValuesForSJis, + new TypeSet("X-IRMC-N"), null) + .addExpectedNodeWithOrder("ADR", + ";\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + + "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + + "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC\u0036" + + "\u968E;;;;150-8512;", + Arrays.asList("", + "\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + + "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + + "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC" + + "\u0036\u968E", "", "", "", "150-8512", ""), + null, mContentValuesForQPAndSJis, new TypeSet("HOME"), null) + .addExpectedNodeWithOrder("NOTE", "\u30E1\u30E2", null, null, + mContentValuesForQPAndSJis, null, null); + } + + public void testV21Japanese2_Type_Generic_Utf8() { + mVerifier.initForImportTest(V21, R.raw.v21_japanese_2); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4") + .put(StructuredName.GIVEN_NAME, "\u30ED\u30A4\u30C9\u0031") + .put(StructuredName.DISPLAY_NAME, + "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9\u0020\u0031") + // ContactStruct should correctly split "SOUND" property into several elements, + // even though VCardParser side does not care it. + .put(StructuredName.PHONETIC_FAMILY_NAME, "\uFF71\uFF9D\uFF84\uFF9E\uFF73") + .put(StructuredName.PHONETIC_GIVEN_NAME, "\uFF9B\uFF72\uFF84\uFF9E\u0031"); + elem.addExpected(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.POSTCODE, "150-8512") + .put(StructuredPostal.STREET, + "\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + + "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + + "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC" + + "\u0036\u968E") + .put(StructuredPostal.FORMATTED_ADDRESS, + "\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + + "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + + "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC" + + "\u0036\u968E 150-8512") + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME); + elem.addExpected(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "\u30E1\u30E2"); + } + + public void testV21MultipleEntryCase_Parse() { + mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_multiple_entry); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("VERSION", "2.1") + .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033;;;;", + Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0033", "", "", "", ""), + null, mContentValuesForSJis, null, null) + .addExpectedNodeWithOrder("SOUND", + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0033;;;;", + null, null, mContentValuesForSJis, + new TypeSet("X-IRMC-N"), null) + .addExpectedNodeWithOrder("TEL", "9", new TypeSet("X-NEC-SECRET")) + .addExpectedNodeWithOrder("TEL", "10", new TypeSet("X-NEC-HOTEL")) + .addExpectedNodeWithOrder("TEL", "11", new TypeSet("X-NEC-SCHOOL")) + .addExpectedNodeWithOrder("TEL", "12", new TypeSet("FAX", "HOME")); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("VERSION", "2.1") + .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034;;;;", + Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0034", "", "", "", ""), + null, mContentValuesForSJis, null, null) + .addExpectedNodeWithOrder("SOUND", + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0034;;;;", + null, null, mContentValuesForSJis, + new TypeSet("X-IRMC-N"), null) + .addExpectedNodeWithOrder("TEL", "13", new TypeSet("MODEM")) + .addExpectedNodeWithOrder("TEL", "14", new TypeSet("PAGER")) + .addExpectedNodeWithOrder("TEL", "15", new TypeSet("X-NEC-FAMILY")) + .addExpectedNodeWithOrder("TEL", "16", new TypeSet("X-NEC-GIRL")); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("VERSION", "2.1") + .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035;;;;", + Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0035", "", "", "", ""), + null, mContentValuesForSJis, null, null) + .addExpectedNodeWithOrder("SOUND", + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0035;;;;", + null, null, mContentValuesForSJis, + new TypeSet("X-IRMC-N"), null) + .addExpectedNodeWithOrder("TEL", "17", new TypeSet("X-NEC-BOY")) + .addExpectedNodeWithOrder("TEL", "18", new TypeSet("X-NEC-FRIEND")) + .addExpectedNodeWithOrder("TEL", "19", new TypeSet("X-NEC-PHS")) + .addExpectedNodeWithOrder("TEL", "20", new TypeSet("X-NEC-RESTAURANT")); + } + + public void testV21MultipleEntryCase() { + mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE, R.raw.v21_multiple_entry); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033") + .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033") + .put(StructuredName.PHONETIC_GIVEN_NAME, + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0033"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-SECRET") + .put(Phone.NUMBER, "9"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-HOTEL") + .put(Phone.NUMBER, "10"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-SCHOOL") + .put(Phone.NUMBER, "11"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_FAX_HOME) + .put(Phone.NUMBER, "12"); + + elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034") + .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034") + .put(StructuredName.PHONETIC_GIVEN_NAME, + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0034"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "MODEM") + .put(Phone.NUMBER, "13"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_PAGER) + .put(Phone.NUMBER, "14"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-FAMILY") + .put(Phone.NUMBER, "15"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-GIRL") + .put(Phone.NUMBER, "16"); + + elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035") + .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035") + .put(StructuredName.PHONETIC_GIVEN_NAME, + "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0035"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-BOY") + .put(Phone.NUMBER, "17"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-FRIEND") + .put(Phone.NUMBER, "18"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-PHS") + .put(Phone.NUMBER, "19"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_CUSTOM) + .put(Phone.LABEL, "NEC-RESTAURANT") + .put(Phone.NUMBER, "20"); + } + + public void testIgnoreAgentV21_Parse() { + mVerifier.initForImportTest(V21, R.raw.v21_winmo_65); + ContentValues contentValuesForValue = new ContentValues(); + contentValuesForValue.put("VALUE", "DATE"); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("VERSION", "2.1") + .addExpectedNodeWithOrder("N", Arrays.asList("Example", "", "", "", "")) + .addExpectedNodeWithOrder("FN", "Example") + .addExpectedNodeWithOrder("ANNIVERSARY", "20091010", contentValuesForValue) + .addExpectedNodeWithOrder("AGENT", "") + .addExpectedNodeWithOrder("X-CLASS", "PUBLIC") + .addExpectedNodeWithOrder("X-REDUCTION", "") + .addExpectedNodeWithOrder("X-NO", ""); + } + + public void testIgnoreAgentV21() { + mVerifier.initForImportTest(V21, R.raw.v21_winmo_65); + ContentValuesVerifier verifier = new ContentValuesVerifier(); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "Example") + .put(StructuredName.DISPLAY_NAME, "Example"); + } + + public void testTolerateInvalidCommentLikeLineV21() { + mVerifier.initForImportTest(V21, R.raw.v21_invalid_comment_line); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.GIVEN_NAME, "Conference Call") + .put(StructuredName.DISPLAY_NAME, "Conference Call"); + elem.addExpected(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "This is an (sharp ->#<- sharp) example. " + + "This message must NOT be ignored."); + } + + public void testPagerV30_Parse() { + mVerifier.initForImportTest(V30, R.raw.v30_comma_separated); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNodeWithOrder("VERSION", "3.0") + .addExpectedNodeWithOrder("N", Arrays.asList("F", "G", "M", "", "")) + .addExpectedNodeWithOrder("TEL", "6101231234@pagersample.com", + new TypeSet("WORK", "MSG", "PAGER")); + } + + public void testPagerV30() { + mVerifier.initForImportTest(V30, R.raw.v30_comma_separated); + ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); + elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "F") + .put(StructuredName.MIDDLE_NAME, "M") + .put(StructuredName.GIVEN_NAME, "G") + .put(StructuredName.DISPLAY_NAME, "G M F"); + elem.addExpected(Phone.CONTENT_ITEM_TYPE) + .put(Phone.TYPE, Phone.TYPE_PAGER) + .put(Phone.NUMBER, "6101231234@pagersample.com"); + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/VCardJapanizationTests.java b/vcard/tests/src/com/android/vcard/tests/VCardJapanizationTests.java new file mode 100644 index 000000000..0d0b9f10a --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/VCardJapanizationTests.java @@ -0,0 +1,436 @@ +/* + * 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.tests; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; + +import com.android.vcard.VCardConfig; +import com.android.vcard.tests.test_utils.ContactEntry; +import com.android.vcard.tests.test_utils.ContentValuesBuilder; +import com.android.vcard.tests.test_utils.PropertyNodesVerifierElem; +import com.android.vcard.tests.test_utils.PropertyNodesVerifierElem.TypeSet; + +import java.util.Arrays; + +public class VCardJapanizationTests extends VCardTestsBase { + private void testNameUtf8Common(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069") + .put(StructuredName.GIVEN_NAME, "\u3091\u308A\u304B") + .put(StructuredName.MIDDLE_NAME, "B") + .put(StructuredName.PREFIX, "Dr.") + .put(StructuredName.SUFFIX, "Ph.D"); + ContentValues contentValues = + (VCardConfig.isV30(vcardType) ? null : mContentValuesForQPAndUtf8); + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNode("FN", "Dr. \u3075\u308B\u3069 B \u3091\u308A\u304B Ph.D", + contentValues) + .addExpectedNode("N", "\u3075\u308B\u3069;\u3091\u308A\u304B;B;Dr.;Ph.D", + Arrays.asList( + "\u3075\u308B\u3069", "\u3091\u308A\u304B", "B", "Dr.", "Ph.D"), + null, contentValues, null, null); + } + + public void testNameUtf8V21() { + testNameUtf8Common(VCardConfig.VCARD_TYPE_V21_JAPANESE); + } + + public void testNameUtf8V30() { + testNameUtf8Common(VCardConfig.VCARD_TYPE_V30_JAPANESE); + } + + public void testNameShiftJis() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V30_JAPANESE, "Shift_JIS"); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069") + .put(StructuredName.GIVEN_NAME, "\u3091\u308A\u304B") + .put(StructuredName.MIDDLE_NAME, "B") + .put(StructuredName.PREFIX, "Dr.") + .put(StructuredName.SUFFIX, "Ph.D"); + + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNode("FN", "Dr. \u3075\u308B\u3069 B \u3091\u308A\u304B Ph.D", + mContentValuesForSJis) + .addExpectedNode("N", "\u3075\u308B\u3069;\u3091\u308A\u304B;B;Dr.;Ph.D", + Arrays.asList( + "\u3075\u308B\u3069", "\u3091\u308A\u304B", "B", "Dr.", "Ph.D"), + null, mContentValuesForSJis, null, null); + } + + /** + * DoCoMo phones require all name elements should be in "family name" field. + */ + public void testNameDoCoMo() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS"); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069") + .put(StructuredName.GIVEN_NAME, "\u3091\u308A\u304B") + .put(StructuredName.MIDDLE_NAME, "B") + .put(StructuredName.PREFIX, "Dr.") + .put(StructuredName.SUFFIX, "Ph.D"); + + final String fullName = "Dr. \u3075\u308B\u3069 B \u3091\u308A\u304B Ph.D"; + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNode("N", fullName + ";;;;", + Arrays.asList(fullName, "", "", "", ""), + null, mContentValuesForSJis, null, null) + .addExpectedNode("FN", fullName, mContentValuesForSJis) + .addExpectedNode("SOUND", ";;;;", new TypeSet("X-IRMC-N")) + .addExpectedNode("TEL", "", new TypeSet("HOME")) + .addExpectedNode("EMAIL", "", new TypeSet("HOME")) + .addExpectedNode("ADR", "", new TypeSet("HOME")) + .addExpectedNode("X-CLASS", "PUBLIC") + .addExpectedNode("X-REDUCTION", "") + .addExpectedNode("X-NO", "") + .addExpectedNode("X-DCM-HMN-MODE", ""); + } + + private void testPhoneticNameCommon(int vcardType, String charset) { + mVerifier.initForExportTest(vcardType, charset); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "\u30DF\u30C9\u30EB\u30CD\u30FC\u30E0") + .put(StructuredName.PHONETIC_GIVEN_NAME, "\u305F\u308D\u3046"); + + final ContentValues contentValues = + ("SHIFT_JIS".equalsIgnoreCase(charset) ? + (VCardConfig.isV30(vcardType) ? mContentValuesForSJis : + mContentValuesForQPAndSJis) : + (VCardConfig.isV30(vcardType) ? null : mContentValuesForQPAndUtf8)); + PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElemWithEmptyName(); + elem.addExpectedNode("X-PHONETIC-LAST-NAME", "\u3084\u307E\u3060", + contentValues) + .addExpectedNode("X-PHONETIC-MIDDLE-NAME", + "\u30DF\u30C9\u30EB\u30CD\u30FC\u30E0", + contentValues) + .addExpectedNode("X-PHONETIC-FIRST-NAME", "\u305F\u308D\u3046", + contentValues); + if (VCardConfig.isV30(vcardType)) { + elem.addExpectedNode("SORT-STRING", + "\u3084\u307E\u3060 \u30DF\u30C9\u30EB\u30CD\u30FC\u30E0 \u305F\u308D\u3046", + contentValues); + } + ContentValuesBuilder builder = mVerifier.addContentValuesVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE); + builder.put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "\u30DF\u30C9\u30EB\u30CD\u30FC\u30E0") + .put(StructuredName.PHONETIC_GIVEN_NAME, "\u305F\u308D\u3046") + .put(StructuredName.DISPLAY_NAME, + "\u3084\u307E\u3060 \u30DF\u30C9\u30EB\u30CD\u30FC\u30E0 " + + "\u305F\u308D\u3046"); + } + + public void testPhoneticNameForJapaneseV21Utf8() { + testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE, null); + } + + public void testPhoneticNameForJapaneseV21Sjis() { + testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE, "Shift_JIS"); + } + + public void testPhoneticNameForJapaneseV30Utf8() { + testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE, null); + } + + public void testPhoneticNameForJapaneseV30SJis() { + testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE, "Shift_JIS"); + } + + public void testPhoneticNameForMobileV21_1() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE, "Shift_JIS"); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060") + .put(StructuredName.PHONETIC_MIDDLE_NAME, "\u30DF\u30C9\u30EB\u30CD\u30FC\u30E0") + .put(StructuredName.PHONETIC_GIVEN_NAME, "\u305F\u308D\u3046"); + + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNode("SOUND", + "\uFF94\uFF8F\uFF80\uFF9E \uFF90\uFF84\uFF9E\uFF99\uFF88\uFF70\uFF91 " + + "\uFF80\uFF9B\uFF73;;;;", + mContentValuesForSJis, new TypeSet("X-IRMC-N")); + ContentValuesBuilder builder = mVerifier.addContentValuesVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE); + builder.put(StructuredName.PHONETIC_FAMILY_NAME, "\uFF94\uFF8F\uFF80\uFF9E") + .put(StructuredName.PHONETIC_MIDDLE_NAME, + "\uFF90\uFF84\uFF9E\uFF99\uFF88\uFF70\uFF91") + .put(StructuredName.PHONETIC_GIVEN_NAME, "\uFF80\uFF9B\uFF73") + .put(StructuredName.DISPLAY_NAME, + "\uFF94\uFF8F\uFF80\uFF9E \uFF90\uFF84\uFF9E\uFF99\uFF88\uFF70\uFF91 " + + "\uFF80\uFF9B\uFF73"); + } + + public void testPhoneticNameForMobileV21_2() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE, "Shift_JIS"); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) + .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060") + .put(StructuredName.PHONETIC_GIVEN_NAME, "\u305F\u308D\u3046"); + + mVerifier.addPropertyNodesVerifierElem() + .addExpectedNode("SOUND", "\uFF94\uFF8F\uFF80\uFF9E \uFF80\uFF9B\uFF73;;;;", + mContentValuesForSJis, new TypeSet("X-IRMC-N")); + ContentValuesBuilder builder = mVerifier.addContentValuesVerifierElem() + .addExpected(StructuredName.CONTENT_ITEM_TYPE); + builder.put(StructuredName.PHONETIC_FAMILY_NAME, "\uFF94\uFF8F\uFF80\uFF9E") + .put(StructuredName.PHONETIC_GIVEN_NAME, "\uFF80\uFF9B\uFF73") + .put(StructuredName.DISPLAY_NAME, "\uFF94\uFF8F\uFF80\uFF9E \uFF80\uFF9B\uFF73"); + } + + private void testPostalAddressWithJapaneseCommon(int vcardType, String charset) { + mVerifier.initForExportTest(vcardType, charset); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.POBOX, "\u79C1\u66F8\u7BB107") + .put(StructuredPostal.STREET, "\u96DB\u898B\u6CA2\u6751") + .put(StructuredPostal.CITY, "\u9E7F\u9AA8\u5E02") + .put(StructuredPostal.REGION, "\u00D7\u00D7\u770C") + .put(StructuredPostal.POSTCODE, "494-1313") + .put(StructuredPostal.COUNTRY, "\u65E5\u672C") + .put(StructuredPostal.FORMATTED_ADDRESS, + "\u3053\u3093\u306A\u3068\u3053\u308D\u3092\u898B" + + "\u308B\u306A\u3093\u3066\u6687\u4EBA\u3067\u3059\u304B\uFF1F") + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM) + .put(StructuredPostal.LABEL, "\u304A\u3082\u3061\u304B\u3048\u308A"); + + ContentValues contentValues = ("UTF-8".equalsIgnoreCase(charset) ? + (VCardConfig.isV30(vcardType) ? mContentValuesForSJis : + mContentValuesForQPAndSJis) : + (VCardConfig.isV30(vcardType) ? mContentValuesForUtf8 : + mContentValuesForQPAndUtf8)); + + PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElemWithEmptyName(); + // LABEL must be ignored in vCard 2.1. As for vCard 3.0, the current behavior is + // same as that in vCard 3.0, which can be changed in the future. + elem.addExpectedNode("ADR", Arrays.asList("\u79C1\u66F8\u7BB107", + "", "\u96DB\u898B\u6CA2\u6751", "\u9E7F\u9AA8\u5E02", "\u00D7\u00D7\u770C", + "494-1313", "\u65E5\u672C"), + contentValues); + mVerifier.addContentValuesVerifierElem().addExpected(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.POBOX, "\u79C1\u66F8\u7BB107") + .put(StructuredPostal.STREET, "\u96DB\u898B\u6CA2\u6751") + .put(StructuredPostal.CITY, "\u9E7F\u9AA8\u5E02") + .put(StructuredPostal.REGION, "\u00D7\u00D7\u770C") + .put(StructuredPostal.POSTCODE, "494-1313") + .put(StructuredPostal.COUNTRY, "\u65E5\u672C") + .put(StructuredPostal.FORMATTED_ADDRESS, + "\u65E5\u672C 494-1313 \u00D7\u00D7\u770C \u9E7F\u9AA8\u5E02 " + + "\u96DB\u898B\u6CA2\u6751 " + "\u79C1\u66F8\u7BB107") + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME); + } + public void testPostalAddresswithJapaneseV21() { + testPostalAddressWithJapaneseCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE, "Shift_JIS"); + } + + /** + * Verifies that only one address field is emitted toward DoCoMo phones. + * Prefered type must (should?) be: HOME > WORK > OTHER > CUSTOM + */ + public void testPostalAdrressForDoCoMo_1() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS"); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK) + .put(StructuredPostal.POBOX, "1"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_OTHER) + .put(StructuredPostal.POBOX, "2"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME) + .put(StructuredPostal.POBOX, "3"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM) + .put(StructuredPostal.LABEL, "custom") + .put(StructuredPostal.POBOX, "4"); + + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "", new TypeSet("HOME")) + .addExpectedNode("EMAIL", "", new TypeSet("HOME")) + .addExpectedNode("X-CLASS", "PUBLIC") + .addExpectedNode("X-REDUCTION", "") + .addExpectedNode("X-NO", "") + .addExpectedNode("X-DCM-HMN-MODE", "") + .addExpectedNode("ADR", + Arrays.asList("3", "", "", "", "", "", ""), new TypeSet("HOME")); + } + + public void testPostalAdrressForDoCoMo_2() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS"); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_OTHER) + .put(StructuredPostal.POBOX, "1"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK) + .put(StructuredPostal.POBOX, "2"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM) + .put(StructuredPostal.LABEL, "custom") + .put(StructuredPostal.POBOX, "3"); + + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "", new TypeSet("HOME")) + .addExpectedNode("EMAIL", "", new TypeSet("HOME")) + .addExpectedNode("X-CLASS", "PUBLIC") + .addExpectedNode("X-REDUCTION", "") + .addExpectedNode("X-NO", "") + .addExpectedNode("X-DCM-HMN-MODE", "") + .addExpectedNode("ADR", + Arrays.asList("2", "", "", "", "", "", ""), new TypeSet("WORK")); + } + + public void testPostalAdrressForDoCoMo_3() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS"); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM) + .put(StructuredPostal.LABEL, "custom1") + .put(StructuredPostal.POBOX, "1"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_OTHER) + .put(StructuredPostal.POBOX, "2"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM) + .put(StructuredPostal.LABEL, "custom2") + .put(StructuredPostal.POBOX, "3"); + + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "", new TypeSet("HOME")) + .addExpectedNode("EMAIL", "", new TypeSet("HOME")) + .addExpectedNode("X-CLASS", "PUBLIC") + .addExpectedNode("X-REDUCTION", "") + .addExpectedNode("X-NO", "") + .addExpectedNode("X-DCM-HMN-MODE", "") + .addExpectedNode("ADR", Arrays.asList("2", "", "", "", "", "", "")); + } + + /** + * Verifies the vCard exporter tolerates null TYPE. + */ + public void testPostalAdrressForDoCoMo_4() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS"); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.POBOX, "1"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_OTHER) + .put(StructuredPostal.POBOX, "2"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME) + .put(StructuredPostal.POBOX, "3"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK) + .put(StructuredPostal.POBOX, "4"); + entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) + .put(StructuredPostal.POBOX, "5"); + + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "", new TypeSet("HOME")) + .addExpectedNode("EMAIL", "", new TypeSet("HOME")) + .addExpectedNode("X-CLASS", "PUBLIC") + .addExpectedNode("X-REDUCTION", "") + .addExpectedNode("X-NO", "") + .addExpectedNode("X-DCM-HMN-MODE", "") + .addExpectedNode("ADR", + Arrays.asList("3", "", "", "", "", "", ""), new TypeSet("HOME")); + } + + private void testJapanesePhoneNumberCommon(int vcardType) { + mVerifier.initForExportTest(vcardType); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "0312341234") + .put(Phone.TYPE, Phone.TYPE_HOME); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "09012341234") + .put(Phone.TYPE, Phone.TYPE_MOBILE); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "03-1234-1234", new TypeSet("HOME")) + .addExpectedNode("TEL", "090-1234-1234", new TypeSet("CELL")); + } + + public void testJapanesePhoneNumberV21_1() { + testJapanesePhoneNumberCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE); + } + + public void testJapanesePhoneNumberV30() { + testJapanesePhoneNumberCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE); + } + + public void testJapanesePhoneNumberDoCoMo() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS"); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "0312341234") + .put(Phone.TYPE, Phone.TYPE_HOME); + entry.addContentValues(Phone.CONTENT_ITEM_TYPE) + .put(Phone.NUMBER, "09012341234") + .put(Phone.TYPE, Phone.TYPE_MOBILE); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("EMAIL", "", new TypeSet("HOME")) + .addExpectedNode("X-CLASS", "PUBLIC") + .addExpectedNode("X-REDUCTION", "") + .addExpectedNode("X-NO", "") + .addExpectedNode("X-DCM-HMN-MODE", "") + .addExpectedNode("ADR", "", new TypeSet("HOME")) + .addExpectedNode("TEL", "03-1234-1234", new TypeSet("HOME")) + .addExpectedNode("TEL", "090-1234-1234", new TypeSet("CELL")); + } + + public void testNoteDoCoMo() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO, "Shift_JIS"); + ContactEntry entry = mVerifier.addInputEntry(); + entry.addContentValues(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "note1"); + entry.addContentValues(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "note2"); + entry.addContentValues(Note.CONTENT_ITEM_TYPE) + .put(Note.NOTE, "note3"); + + // More than one note fields must be aggregated into one note. + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("TEL", "", new TypeSet("HOME")) + .addExpectedNode("EMAIL", "", new TypeSet("HOME")) + .addExpectedNode("X-CLASS", "PUBLIC") + .addExpectedNode("X-REDUCTION", "") + .addExpectedNode("X-NO", "") + .addExpectedNode("X-DCM-HMN-MODE", "") + .addExpectedNode("ADR", "", new TypeSet("HOME")) + .addExpectedNode("NOTE", "note1\nnote2\nnote3", mContentValuesForQP); + } + + public void testAndroidCustomV21() { + mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_GENERIC); + mVerifier.addInputEntry().addContentValues(Nickname.CONTENT_ITEM_TYPE) + .put(Nickname.NAME, "\u304D\u3083\u30FC\u30A8\u30C3\u30C1\u30FC"); + mVerifier.addPropertyNodesVerifierElemWithEmptyName() + .addExpectedNode("X-ANDROID-CUSTOM", + Arrays.asList(Nickname.CONTENT_ITEM_TYPE, + "\u304D\u3083\u30FC\u30A8\u30C3\u30C1\u30FC", + "", "", "", "", "", "", "", "", "", "", "", "", "", ""), + mContentValuesForQPAndUtf8); + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/VCardTestsBase.java b/vcard/tests/src/com/android/vcard/tests/VCardTestsBase.java new file mode 100644 index 000000000..8998b3caf --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/VCardTestsBase.java @@ -0,0 +1,85 @@ +/* + * 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.tests; + +import android.content.ContentValues; +import android.test.AndroidTestCase; + +import com.android.vcard.VCardConfig; +import com.android.vcard.tests.test_utils.VCardVerifier; + +/** + * BaseClass for vCard unit tests with utility classes. + * Please do not add each unit test here. + */ +/* package */ class VCardTestsBase extends AndroidTestCase { + public static final int V21 = VCardConfig.VCARD_TYPE_V21_GENERIC; + public static final int V30 = VCardConfig.VCARD_TYPE_V30_GENERIC; + + // Do not modify these during tests. + protected final ContentValues mContentValuesForQP; + protected final ContentValues mContentValuesForSJis; + protected final ContentValues mContentValuesForUtf8; + protected final ContentValues mContentValuesForQPAndSJis; + protected final ContentValues mContentValuesForQPAndUtf8; + protected final ContentValues mContentValuesForBase64V21; + protected final ContentValues mContentValuesForBase64V30; + + protected VCardVerifier mVerifier; + private boolean mSkipVerification; + + public VCardTestsBase() { + super(); + // Not using constants in vCard code since it may be wrong. + mContentValuesForQP = new ContentValues(); + mContentValuesForQP.put("ENCODING", "QUOTED-PRINTABLE"); + mContentValuesForSJis = new ContentValues(); + mContentValuesForSJis.put("CHARSET", "SHIFT_JIS"); + mContentValuesForUtf8 = new ContentValues(); + mContentValuesForUtf8.put("CHARSET", "UTF-8"); + mContentValuesForQPAndSJis = new ContentValues(); + mContentValuesForQPAndSJis.put("ENCODING", "QUOTED-PRINTABLE"); + mContentValuesForQPAndSJis.put("CHARSET", "SHIFT_JIS"); + mContentValuesForQPAndUtf8 = new ContentValues(); + mContentValuesForQPAndUtf8.put("ENCODING", "QUOTED-PRINTABLE"); + mContentValuesForQPAndUtf8.put("CHARSET", "UTF-8"); + mContentValuesForBase64V21 = new ContentValues(); + mContentValuesForBase64V21.put("ENCODING", "BASE64"); + mContentValuesForBase64V30 = new ContentValues(); + mContentValuesForBase64V30.put("ENCODING", "b"); + } + + @Override + public void testAndroidTestCaseSetupProperly() { + super.testAndroidTestCaseSetupProperly(); + mSkipVerification = true; + } + + @Override + public void setUp() throws Exception{ + super.setUp(); + mVerifier = new VCardVerifier(this); + mSkipVerification = false; + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + if (!mSkipVerification) { + mVerifier.verify(); + } + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/VCardUtilsTests.java b/vcard/tests/src/com/android/vcard/tests/VCardUtilsTests.java new file mode 100644 index 000000000..732009a36 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/VCardUtilsTests.java @@ -0,0 +1,84 @@ +/* + * 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.tests; + +import com.android.vcard.VCardUtils; + +import junit.framework.TestCase; + +import java.util.List; + +public class VCardUtilsTests extends TestCase { + public void testContainsOnlyPrintableAscii() { + assertTrue(VCardUtils.containsOnlyPrintableAscii((String)null)); + assertTrue(VCardUtils.containsOnlyPrintableAscii((String[])null)); + assertTrue(VCardUtils.containsOnlyPrintableAscii((List)null)); + assertTrue(VCardUtils.containsOnlyPrintableAscii("")); + assertTrue(VCardUtils.containsOnlyPrintableAscii("abcdefghijklmnopqrstuvwxyz")); + assertTrue(VCardUtils.containsOnlyPrintableAscii("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + StringBuilder builder = new StringBuilder(); + for (int i = 0x20; i < 0x7F; i++) { + builder.append((char)i); + } + assertTrue(VCardUtils.containsOnlyPrintableAscii(builder.toString())); + assertTrue(VCardUtils.containsOnlyPrintableAscii("\r\n")); + assertFalse(VCardUtils.containsOnlyPrintableAscii("\u0019")); + assertFalse(VCardUtils.containsOnlyPrintableAscii("\u007F")); + } + + public void testContainsOnlyNonCrLfPrintableAscii() { + assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii((String)null)); + assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii((String[])null)); + assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii((List)null)); + assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("")); + assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("abcdefghijklmnopqrstuvwxyz")); + assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + StringBuilder builder = new StringBuilder(); + for (int i = 0x20; i < 0x7F; i++) { + builder.append((char)i); + } + assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii(builder.toString())); + assertFalse(VCardUtils.containsOnlyNonCrLfPrintableAscii("\u0019")); + assertFalse(VCardUtils.containsOnlyNonCrLfPrintableAscii("\u007F")); + assertFalse(VCardUtils.containsOnlyNonCrLfPrintableAscii("\r")); + assertFalse(VCardUtils.containsOnlyNonCrLfPrintableAscii("\n")); + } + + public void testContainsOnlyAlphaDigitHyphen() { + assertTrue(VCardUtils.containsOnlyAlphaDigitHyphen((String)null)); + assertTrue(VCardUtils.containsOnlyAlphaDigitHyphen((String[])null)); + assertTrue(VCardUtils.containsOnlyAlphaDigitHyphen((List)null)); + assertTrue(VCardUtils.containsOnlyAlphaDigitHyphen("")); + assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("abcdefghijklmnopqrstuvwxyz")); + assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("0123456789-")); + for (int i = 0; i < 0x30; i++) { + if (i == 0x2D) { // - + continue; + } + assertFalse(VCardUtils.containsOnlyAlphaDigitHyphen(String.valueOf((char)i))); + } + for (int i = 0x3A; i < 0x41; i++) { + assertFalse(VCardUtils.containsOnlyAlphaDigitHyphen(String.valueOf((char)i))); + } + for (int i = 0x5B; i < 0x61; i++) { + assertFalse(VCardUtils.containsOnlyAlphaDigitHyphen(String.valueOf((char)i))); + } + for (int i = 0x7B; i < 0x100; i++) { + assertFalse(VCardUtils.containsOnlyAlphaDigitHyphen(String.valueOf((char)i))); + } + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/ContactEntry.java b/vcard/tests/src/com/android/vcard/tests/test_utils/ContactEntry.java new file mode 100644 index 000000000..dff1f0510 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/ContactEntry.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.vcard.tests.test_utils; + +import android.content.ContentValues; +import android.provider.ContactsContract.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * The class representing one contact, which should contain multiple ContentValues like + * StructuredName, Email, etc. + *

+ */ +public final class ContactEntry { + private final List mContentValuesList = new ArrayList(); + + public ContentValuesBuilder addContentValues(String mimeType) { + ContentValues contentValues = new ContentValues(); + contentValues.put(Data.MIMETYPE, mimeType); + mContentValuesList.add(contentValues); + return new ContentValuesBuilder(contentValues); + } + + public List getList() { + return mContentValuesList; + } +} \ No newline at end of file diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java b/vcard/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java new file mode 100644 index 000000000..fb53b8f9a --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java @@ -0,0 +1,80 @@ +/* + * 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.tests.test_utils; + +import android.content.ContentValues; + +/** + * ContentValues-like class which enables users to chain put() methods and restricts + * the other methods. + */ +public class ContentValuesBuilder { + private final ContentValues mContentValues; + + public ContentValuesBuilder(final ContentValues contentValues) { + mContentValues = contentValues; + } + + public ContentValuesBuilder put(String key, String value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Byte value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Short value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Integer value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Long value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Float value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Double value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, Boolean value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder put(String key, byte[] value) { + mContentValues.put(key, value); + return this; + } + + public ContentValuesBuilder putNull(String key) { + mContentValues.putNull(key); + return this; + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.java b/vcard/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.java new file mode 100644 index 000000000..7d6db53f3 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.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.tests.test_utils; + +import android.test.AndroidTestCase; + +import com.android.vcard.VCardConfig; +import com.android.vcard.VCardEntry; +import com.android.vcard.VCardEntryConstructor; +import com.android.vcard.VCardEntryHandler; +import com.android.vcard.VCardParser; +import com.android.vcard.VCardParser_V21; +import com.android.vcard.VCardParser_V30; +import com.android.vcard.exception.VCardException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class ContentValuesVerifier implements VCardEntryHandler { + private AndroidTestCase mTestCase; + private List mContentValuesVerifierElemList = + new ArrayList(); + private int mIndex; + + public ContentValuesVerifierElem addElem(AndroidTestCase androidTestCase) { + mTestCase = androidTestCase; + ContentValuesVerifierElem importVerifier = new ContentValuesVerifierElem(androidTestCase); + mContentValuesVerifierElemList.add(importVerifier); + return importVerifier; + } + + public void verify(int resId, int vCardType) throws IOException, VCardException { + verify(mTestCase.getContext().getResources().openRawResource(resId), vCardType); + } + + public void verify(int resId, int vCardType, final VCardParser vCardParser) + throws IOException, VCardException { + verify(mTestCase.getContext().getResources().openRawResource(resId), + vCardType, vCardParser); + } + + public void verify(InputStream is, int vCardType) throws IOException, VCardException { + final VCardParser vCardParser; + if (VCardConfig.isV30(vCardType)) { + vCardParser = new VCardParser_V30(); + } else { + vCardParser = new VCardParser_V21(); + } + verify(is, vCardType, vCardParser); + } + + public void verify(InputStream is, int vCardType, final VCardParser vCardParser) + throws IOException, VCardException { + VCardEntryConstructor builder = + new VCardEntryConstructor(vCardType, null, null, false); + builder.addEntryHandler(this); + try { + vCardParser.parse(is, builder); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + } + + public void onStart() { + for (ContentValuesVerifierElem elem : mContentValuesVerifierElemList) { + elem.onParsingStart(); + } + } + + public void onEntryCreated(VCardEntry entry) { + mTestCase.assertTrue(mIndex < mContentValuesVerifierElemList.size()); + mContentValuesVerifierElemList.get(mIndex).onEntryCreated(entry); + mIndex++; + } + + public void onEnd() { + for (ContentValuesVerifierElem elem : mContentValuesVerifierElemList) { + elem.onParsingEnd(); + elem.verifyResolver(); + } + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifierElem.java b/vcard/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifierElem.java new file mode 100644 index 000000000..ecf4a2b69 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifierElem.java @@ -0,0 +1,96 @@ +/* + * 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.tests.test_utils; + +import android.content.ContentValues; +import android.provider.ContactsContract.Data; +import android.test.AndroidTestCase; + +import com.android.vcard.VCardConfig; +import com.android.vcard.VCardEntry; +import com.android.vcard.VCardEntryCommitter; +import com.android.vcard.VCardEntryConstructor; +import com.android.vcard.VCardEntryHandler; +import com.android.vcard.VCardParser; +import com.android.vcard.VCardParser_V21; +import com.android.vcard.VCardParser_V30; +import com.android.vcard.exception.VCardException; + +import java.io.IOException; +import java.io.InputStream; + +public class ContentValuesVerifierElem { + private final AndroidTestCase mTestCase; + private final ImportTestResolver mResolver; + private final VCardEntryHandler mHandler; + + public ContentValuesVerifierElem(AndroidTestCase androidTestCase) { + mTestCase = androidTestCase; + mResolver = new ImportTestResolver(androidTestCase); + mHandler = new VCardEntryCommitter(mResolver); + } + + public ContentValuesBuilder addExpected(String mimeType) { + ContentValues contentValues = new ContentValues(); + contentValues.put(Data.MIMETYPE, mimeType); + mResolver.addExpectedContentValues(contentValues); + return new ContentValuesBuilder(contentValues); + } + + public void verify(int resId, int vCardType) + throws IOException, VCardException { + verify(mTestCase.getContext().getResources().openRawResource(resId), vCardType); + } + + public void verify(InputStream is, int vCardType) throws IOException, VCardException { + final VCardParser vCardParser; + if (VCardConfig.isV30(vCardType)) { + vCardParser = new VCardParser_V30(); + } else { + vCardParser = new VCardParser_V21(); + } + VCardEntryConstructor builder = + new VCardEntryConstructor(vCardType, null, null, false); + builder.addEntryHandler(mHandler); + try { + vCardParser.parse(is, builder); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + verifyResolver(); + } + + public void verifyResolver() { + mResolver.verify(); + } + + public void onParsingStart() { + mHandler.onStart(); + } + + public void onEntryCreated(VCardEntry entry) { + mHandler.onEntryCreated(entry); + } + + public void onParsingEnd() { + mHandler.onEnd(); + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/ExportTestProvider.java b/vcard/tests/src/com/android/vcard/tests/test_utils/ExportTestProvider.java new file mode 100644 index 000000000..caedf9dca --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/ExportTestProvider.java @@ -0,0 +1,176 @@ +/* + * 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.tests.test_utils; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Entity; +import android.content.EntityIterator; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import android.test.mock.MockContentProvider; +import android.test.mock.MockCursor; + +import com.android.vcard.VCardComposer; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/* package */ class ExportTestProvider extends MockContentProvider { + final private TestCase mTestCase; + final private ArrayList mContactEntryList = new ArrayList(); + + private static class MockEntityIterator implements EntityIterator { + List mEntityList; + Iterator mIterator; + + public MockEntityIterator(List contentValuesList) { + mEntityList = new ArrayList(); + Entity entity = new Entity(new ContentValues()); + for (ContentValues contentValues : contentValuesList) { + entity.addSubValue(Data.CONTENT_URI, contentValues); + } + mEntityList.add(entity); + mIterator = mEntityList.iterator(); + } + + public boolean hasNext() { + return mIterator.hasNext(); + } + + public Entity next() { + return mIterator.next(); + } + + public void remove() { + throw new UnsupportedOperationException("remove not supported"); + } + + public void reset() { + mIterator = mEntityList.iterator(); + } + + public void close() { + } + } + + public ExportTestProvider(TestCase testCase) { + mTestCase = testCase; + } + + public ContactEntry buildInputEntry() { + ContactEntry contactEntry = new ContactEntry(); + mContactEntryList.add(contactEntry); + return contactEntry; + } + + /** + *

+ * An old method which had existed but was removed from ContentResolver. + *

+ *

+ * We still keep using this method since we don't have a propeer way to know + * which value in the ContentValue corresponds to the entry in Contacts database. + *

+ */ + public EntityIterator queryEntities(Uri uri, + String selection, String[] selectionArgs, String sortOrder) { + mTestCase.assertTrue(uri != null); + mTestCase.assertTrue(ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())); + final String authority = uri.getAuthority(); + mTestCase.assertTrue(RawContacts.CONTENT_URI.getAuthority().equals(authority)); + mTestCase.assertTrue((Data.CONTACT_ID + "=?").equals(selection)); + mTestCase.assertEquals(1, selectionArgs.length); + final int id = Integer.parseInt(selectionArgs[0]); + mTestCase.assertTrue(id >= 0 && id < mContactEntryList.size()); + + return new MockEntityIterator(mContactEntryList.get(id).getList()); + } + + @Override + public Cursor query(Uri uri,String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + mTestCase.assertTrue(VCardComposer.CONTACTS_TEST_CONTENT_URI.equals(uri)); + // In this test, following arguments are not supported. + mTestCase.assertNull(selection); + mTestCase.assertNull(selectionArgs); + mTestCase.assertNull(sortOrder); + + return new MockCursor() { + int mCurrentPosition = -1; + + @Override + public int getCount() { + return mContactEntryList.size(); + } + + @Override + public boolean moveToFirst() { + mCurrentPosition = 0; + return true; + } + + @Override + public boolean moveToNext() { + if (mCurrentPosition < mContactEntryList.size()) { + mCurrentPosition++; + return true; + } else { + return false; + } + } + + @Override + public boolean isBeforeFirst() { + return mCurrentPosition < 0; + } + + @Override + public boolean isAfterLast() { + return mCurrentPosition >= mContactEntryList.size(); + } + + @Override + public int getColumnIndex(String columnName) { + mTestCase.assertEquals(Contacts._ID, columnName); + return 0; + } + + @Override + public int getInt(int columnIndex) { + mTestCase.assertEquals(0, columnIndex); + mTestCase.assertTrue(mCurrentPosition >= 0 + && mCurrentPosition < mContactEntryList.size()); + return mCurrentPosition; + } + + @Override + public String getString(int columnIndex) { + return String.valueOf(getInt(columnIndex)); + } + + @Override + public void close() { + } + }; + } +} \ No newline at end of file diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/ExportTestResolver.java b/vcard/tests/src/com/android/vcard/tests/test_utils/ExportTestResolver.java new file mode 100644 index 000000000..3cd014ce4 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/ExportTestResolver.java @@ -0,0 +1,40 @@ +/* + * 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.tests.test_utils; + +import android.provider.ContactsContract.RawContacts; +import android.test.mock.MockContentResolver; + +import com.android.vcard.VCardComposer; + +import junit.framework.TestCase; + +/* package */ class ExportTestResolver extends MockContentResolver { + private final ExportTestProvider mProvider; + public ExportTestResolver(TestCase testCase) { + mProvider = new ExportTestProvider(testCase); + addProvider(VCardComposer.VCARD_TEST_AUTHORITY, mProvider); + addProvider(RawContacts.CONTENT_URI.getAuthority(), mProvider); + } + + public ContactEntry addInputContactEntry() { + return mProvider.buildInputEntry(); + } + + public ExportTestProvider getProvider() { + return mProvider; + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java b/vcard/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java new file mode 100644 index 000000000..3d7cb60a0 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java @@ -0,0 +1,274 @@ +/* + * 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.tests.test_utils; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentValues; +import android.net.Uri; +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.Relation; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.test.mock.MockContentProvider; +import android.text.TextUtils; +import android.util.Log; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.Map.Entry; + +/* package */ class ImportTestProvider extends MockContentProvider { + private static final Set sKnownMimeTypeSet = + new HashSet(Arrays.asList(StructuredName.CONTENT_ITEM_TYPE, + Nickname.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE, + Email.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE, + Im.CONTENT_ITEM_TYPE, Organization.CONTENT_ITEM_TYPE, + Event.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE, + Note.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE, + Relation.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, + GroupMembership.CONTENT_ITEM_TYPE)); + + final Map> mMimeTypeToExpectedContentValues; + + private final TestCase mTestCase; + + public ImportTestProvider(TestCase testCase) { + mTestCase = testCase; + mMimeTypeToExpectedContentValues = + new HashMap>(); + for (String acceptanbleMimeType : sKnownMimeTypeSet) { + // Do not use HashSet since the current implementation changes the content of + // ContentValues after the insertion, which make the result of hashCode() + // changes... + mMimeTypeToExpectedContentValues.put( + acceptanbleMimeType, new ArrayList()); + } + } + + public void addExpectedContentValues(ContentValues expectedContentValues) { + final String mimeType = expectedContentValues.getAsString(Data.MIMETYPE); + if (!sKnownMimeTypeSet.contains(mimeType)) { + mTestCase.fail(String.format( + "Unknow MimeType %s in the test code. Test code should be broken.", + mimeType)); + } + + final Collection contentValuesCollection = + mMimeTypeToExpectedContentValues.get(mimeType); + contentValuesCollection.add(expectedContentValues); + } + + @Override + public ContentProviderResult[] applyBatch( + ArrayList operations) { + if (operations == null) { + mTestCase.fail("There is no operation."); + } + + final int size = operations.size(); + ContentProviderResult[] fakeResultArray = new ContentProviderResult[size]; + for (int i = 0; i < size; i++) { + Uri uri = Uri.withAppendedPath(RawContacts.CONTENT_URI, String.valueOf(i)); + fakeResultArray[i] = new ContentProviderResult(uri); + } + + for (int i = 0; i < size; i++) { + ContentProviderOperation operation = operations.get(i); + ContentValues contentValues = operation.resolveValueBackReferences( + fakeResultArray, i); + } + for (int i = 0; i < size; i++) { + ContentProviderOperation operation = operations.get(i); + ContentValues actualContentValues = operation.resolveValueBackReferences( + fakeResultArray, i); + final Uri uri = operation.getUri(); + if (uri.equals(RawContacts.CONTENT_URI)) { + mTestCase.assertNull(actualContentValues.get(RawContacts.ACCOUNT_NAME)); + mTestCase.assertNull(actualContentValues.get(RawContacts.ACCOUNT_TYPE)); + } else if (uri.equals(Data.CONTENT_URI)) { + final String mimeType = actualContentValues.getAsString(Data.MIMETYPE); + if (!sKnownMimeTypeSet.contains(mimeType)) { + mTestCase.fail(String.format( + "Unknown MimeType %s. Probably added after developing this test", + mimeType)); + } + // Remove data meaningless in this unit tests. + // Specifically, Data.DATA1 - DATA7 are set to null or empty String + // regardless of the input, but it may change depending on how + // resolver-related code handles it. + // Here, we ignore these implementation-dependent specs and + // just check whether vCard importer correctly inserts rellevent data. + Set keyToBeRemoved = new HashSet(); + for (Entry entry : actualContentValues.valueSet()) { + Object value = entry.getValue(); + if (value == null || TextUtils.isEmpty(value.toString())) { + keyToBeRemoved.add(entry.getKey()); + } + } + for (String key: keyToBeRemoved) { + actualContentValues.remove(key); + } + /* for testing + Log.d("@@@", + String.format("MimeType: %s, data: %s", + mimeType, actualContentValues.toString())); */ + // Remove RAW_CONTACT_ID entry just for safety, since we do not care + // how resolver-related code handles the entry in this unit test, + if (actualContentValues.containsKey(Data.RAW_CONTACT_ID)) { + actualContentValues.remove(Data.RAW_CONTACT_ID); + } + final Collection contentValuesCollection = + mMimeTypeToExpectedContentValues.get(mimeType); + if (contentValuesCollection.isEmpty()) { + mTestCase.fail("ContentValues for MimeType " + mimeType + + " is not expected at all (" + actualContentValues + ")"); + } + boolean checked = false; + for (ContentValues expectedContentValues : contentValuesCollection) { + /*for testing + Log.d("@@@", "expected: " + + convertToEasilyReadableString(expectedContentValues)); + Log.d("@@@", "actual : " + + convertToEasilyReadableString(actualContentValues));*/ + if (equalsForContentValues(expectedContentValues, + actualContentValues)) { + mTestCase.assertTrue(contentValuesCollection.remove(expectedContentValues)); + checked = true; + break; + } + } + if (!checked) { + final StringBuilder builder = new StringBuilder(); + builder.append("Unexpected: "); + builder.append(convertToEasilyReadableString(actualContentValues)); + builder.append("\nExpected: "); + for (ContentValues expectedContentValues : contentValuesCollection) { + builder.append(convertToEasilyReadableString(expectedContentValues)); + } + mTestCase.fail(builder.toString()); + } + } else { + mTestCase.fail("Unexpected Uri has come: " + uri); + } + } // for (int i = 0; i < size; i++) { + return fakeResultArray; + } + + public void verify() { + StringBuilder builder = new StringBuilder(); + for (Collection contentValuesCollection : + mMimeTypeToExpectedContentValues.values()) { + for (ContentValues expectedContentValues: contentValuesCollection) { + builder.append(convertToEasilyReadableString(expectedContentValues)); + builder.append("\n"); + } + } + if (builder.length() > 0) { + final String failMsg = + "There is(are) remaining expected ContentValues instance(s): \n" + + builder.toString(); + mTestCase.fail(failMsg); + } + } + + /** + * Utility method to print ContentValues whose content is printed with sorted keys. + */ + private String convertToEasilyReadableString(ContentValues contentValues) { + if (contentValues == null) { + return "null"; + } + String mimeTypeValue = ""; + SortedMap sortedMap = new TreeMap(); + for (Entry entry : contentValues.valueSet()) { + final String key = entry.getKey(); + final Object value = entry.getValue(); + final String valueString = (value != null ? value.toString() : null); + if (Data.MIMETYPE.equals(key)) { + mimeTypeValue = valueString; + } else { + mTestCase.assertNotNull(key); + sortedMap.put(key, valueString); + } + } + StringBuilder builder = new StringBuilder(); + builder.append(Data.MIMETYPE); + builder.append('='); + builder.append(mimeTypeValue); + for (Entry entry : sortedMap.entrySet()) { + final String key = entry.getKey(); + final String value = entry.getValue(); + builder.append(' '); + builder.append(key); + builder.append("=\""); + builder.append(value); + builder.append('"'); + } + return builder.toString(); + } + + private static boolean equalsForContentValues( + ContentValues expected, ContentValues actual) { + if (expected == actual) { + return true; + } else if (expected == null || actual == null || expected.size() != actual.size()) { + return false; + } + + for (Entry entry : expected.valueSet()) { + final String key = entry.getKey(); + final Object value = entry.getValue(); + if (!actual.containsKey(key)) { + return false; + } + if (value instanceof byte[]) { + Object actualValue = actual.get(key); + if (!Arrays.equals((byte[])value, (byte[])actualValue)) { + byte[] e = (byte[])value; + byte[] a = (byte[])actualValue; + Log.d("@@@", "expected (len: " + e.length + "): " + Arrays.toString(e)); + Log.d("@@@", "actual (len: " + a.length + "): " + Arrays.toString(a)); + return false; + } + } else if (!value.equals(actual.get(key))) { + Log.d("@@@", "different."); + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/ImportTestResolver.java b/vcard/tests/src/com/android/vcard/tests/test_utils/ImportTestResolver.java new file mode 100644 index 000000000..645e9dbea --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/ImportTestResolver.java @@ -0,0 +1,57 @@ +/* + * 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.tests.test_utils; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentValues; +import android.provider.ContactsContract.RawContacts; +import android.test.mock.MockContentResolver; + +import junit.framework.TestCase; + +import java.util.ArrayList; + +/* package */ class ImportTestResolver extends MockContentResolver { + private final ImportTestProvider mProvider; + + public ImportTestResolver(TestCase testCase) { + mProvider = new ImportTestProvider(testCase); + } + + @Override + public ContentProviderResult[] applyBatch(String authority, + ArrayList operations) { + equalsString(authority, RawContacts.CONTENT_URI.toString()); + return mProvider.applyBatch(operations); + } + + public void addExpectedContentValues(ContentValues expectedContentValues) { + mProvider.addExpectedContentValues(expectedContentValues); + } + + public void verify() { + mProvider.verify(); + } + + private static boolean equalsString(String a, String b) { + if (a == null || a.length() == 0) { + return b == null || b.length() == 0; + } else { + return a.equals(b); + } + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/LineVerifier.java b/vcard/tests/src/com/android/vcard/tests/test_utils/LineVerifier.java new file mode 100644 index 000000000..d8cfe5b48 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/LineVerifier.java @@ -0,0 +1,66 @@ +/* + * 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.tests.test_utils; + +import com.android.vcard.VCardComposer; + +import android.content.Context; + +import junit.framework.TestCase; + +import java.util.ArrayList; + +public class LineVerifier implements VCardComposer.OneEntryHandler { + private final TestCase mTestCase; + private final ArrayList mLineVerifierElemList; + private int mVCardType; + private int index; + + public LineVerifier(TestCase testCase, int vcardType) { + mTestCase = testCase; + mLineVerifierElemList = new ArrayList(); + mVCardType = vcardType; + } + + public LineVerifierElem addLineVerifierElem() { + LineVerifierElem lineVerifier = new LineVerifierElem(mTestCase, mVCardType); + mLineVerifierElemList.add(lineVerifier); + return lineVerifier; + } + + public void verify(String vcard) { + if (index >= mLineVerifierElemList.size()) { + mTestCase.fail("Insufficient number of LineVerifier (" + index + ")"); + } + + LineVerifierElem lineVerifier = mLineVerifierElemList.get(index); + lineVerifier.verify(vcard); + + index++; + } + + public boolean onEntryCreated(String vcard) { + verify(vcard); + return true; + } + + public boolean onInit(Context context) { + return true; + } + + public void onTerminate() { + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/LineVerifierElem.java b/vcard/tests/src/com/android/vcard/tests/test_utils/LineVerifierElem.java new file mode 100644 index 000000000..3ec6ba39b --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/LineVerifierElem.java @@ -0,0 +1,106 @@ +/* + * 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.tests.test_utils; + +import android.text.TextUtils; + +import com.android.vcard.VCardConfig; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.List; + +public class LineVerifierElem { + private final TestCase mTestCase; + private final List mExpectedLineList = new ArrayList(); + private final boolean mIsV30; + + public LineVerifierElem(TestCase testCase, int vcardType) { + mTestCase = testCase; + mIsV30 = VCardConfig.isV30(vcardType); + } + + public LineVerifierElem addExpected(final String line) { + if (!TextUtils.isEmpty(line)) { + mExpectedLineList.add(line); + } + return this; + } + + public void verify(final String vcard) { + final String[] lineArray = vcard.split("\\r?\\n"); + final int length = lineArray.length; + boolean beginExists = false; + boolean endExists = false; + boolean versionExists = false; + + for (int i = 0; i < length; i++) { + final String line = lineArray[i]; + if (TextUtils.isEmpty(line)) { + continue; + } + + if ("BEGIN:VCARD".equalsIgnoreCase(line)) { + if (beginExists) { + mTestCase.fail("Multiple \"BEGIN:VCARD\" line found"); + } else { + beginExists = true; + continue; + } + } else if ("END:VCARD".equalsIgnoreCase(line)) { + if (endExists) { + mTestCase.fail("Multiple \"END:VCARD\" line found"); + } else { + endExists = true; + continue; + } + } else if ((mIsV30 ? "VERSION:3.0" : "VERSION:2.1").equalsIgnoreCase(line)) { + if (versionExists) { + mTestCase.fail("Multiple VERSION line + found"); + } else { + versionExists = true; + continue; + } + } + + if (!beginExists) { + mTestCase.fail("Property other than BEGIN came before BEGIN property: " + + line); + } else if (endExists) { + mTestCase.fail("Property other than END came after END property: " + + line); + } + + final int index = mExpectedLineList.indexOf(line); + if (index >= 0) { + mExpectedLineList.remove(index); + } else { + mTestCase.fail("Unexpected line: " + line); + } + } + + if (!mExpectedLineList.isEmpty()) { + StringBuffer buffer = new StringBuffer(); + for (String expectedLine : mExpectedLineList) { + buffer.append(expectedLine); + buffer.append("\n"); + } + + mTestCase.fail("Expected line(s) not found:" + buffer.toString()); + } + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/PropertyNode.java b/vcard/tests/src/com/android/vcard/tests/test_utils/PropertyNode.java new file mode 100644 index 000000000..14c8d6cb2 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/PropertyNode.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.vcard.tests.test_utils; + +import android.content.ContentValues; + +import com.android.vcard.VCardEntry; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + *

+ * The class representing one property (e.g. "N;ENCODING=UTF-8:family:given:middle:prefix:suffix"). + *

+ *

+ * Previously used in main vCard handling code but now exists only for testing. + *

+ *

+ * Especially useful for testing parser code (VCardParser), since all properties can be + * checked via this class unlike {@link VCardEntry}, which only emits the result of + * interpretation of the content of each vCard. We cannot know whether vCard parser or + * {@link VCardEntry} is wrong without this class. + *

+ */ +public class PropertyNode { + public String propName; + public String propValue; + public List propValue_vector; + + /** Store value as byte[],after decode. + * Used when propValue is encoded by something like BASE64, QUOTED-PRINTABLE, etc. + */ + public byte[] propValue_bytes; + + /** + * param store: key=paramType, value=paramValue + * Note that currently PropertyNode class does not support multiple param-values + * defined in vCard 3.0 (See also RFC 2426). multiple-values are stored as + * one String value like "A,B", not ["A", "B"]... + * TODO: fix this. + */ + public ContentValues paramMap; + + /** Only for TYPE=??? param store. */ + public Set paramMap_TYPE; + + /** Store group values. Used only in VCard. */ + public Set propGroupSet; + + public PropertyNode() { + propName = ""; + propValue = ""; + propValue_vector = new ArrayList(); + paramMap = new ContentValues(); + paramMap_TYPE = new HashSet(); + propGroupSet = new HashSet(); + } + + public PropertyNode( + String propName, String propValue, List propValue_vector, + byte[] propValue_bytes, ContentValues paramMap, Set paramMap_TYPE, + Set propGroupSet) { + if (propName != null) { + this.propName = propName; + } else { + this.propName = ""; + } + if (propValue != null) { + this.propValue = propValue; + } else { + this.propValue = ""; + } + if (propValue_vector != null) { + this.propValue_vector = propValue_vector; + } else { + this.propValue_vector = new ArrayList(); + } + this.propValue_bytes = propValue_bytes; + if (paramMap != null) { + this.paramMap = paramMap; + } else { + this.paramMap = new ContentValues(); + } + if (paramMap_TYPE != null) { + this.paramMap_TYPE = paramMap_TYPE; + } else { + this.paramMap_TYPE = new HashSet(); + } + if (propGroupSet != null) { + this.propGroupSet = propGroupSet; + } else { + this.propGroupSet = new HashSet(); + } + } + + @Override + public int hashCode() { + // vCard may contain more than one same line in one entry, while HashSet or any other + // library which utilize hashCode() does not honor that, so intentionally throw an + // Exception. + throw new UnsupportedOperationException( + "PropertyNode does not provide hashCode() implementation intentionally."); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PropertyNode)) { + return false; + } + + PropertyNode node = (PropertyNode)obj; + + if (propName == null || !propName.equals(node.propName)) { + return false; + } else if (!paramMap_TYPE.equals(node.paramMap_TYPE)) { + return false; + } else if (!paramMap_TYPE.equals(node.paramMap_TYPE)) { + return false; + } else if (!propGroupSet.equals(node.propGroupSet)) { + return false; + } + + if (propValue_bytes != null && Arrays.equals(propValue_bytes, node.propValue_bytes)) { + return true; + } else { + if (!propValue.equals(node.propValue)) { + return false; + } + + // The value in propValue_vector is not decoded even if it should be + // decoded by BASE64 or QUOTED-PRINTABLE. When the size of propValue_vector + // is 1, the encoded value is stored in propValue, so we do not have to + // check it. + return (propValue_vector.equals(node.propValue_vector) || + propValue_vector.size() == 1 || + node.propValue_vector.size() == 1); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("propName: "); + builder.append(propName); + builder.append(", paramMap: "); + builder.append(paramMap.toString()); + builder.append(", paramMap_TYPE: ["); + boolean first = true; + for (String elem : paramMap_TYPE) { + if (first) { + first = false; + } else { + builder.append(", "); + } + builder.append('"'); + builder.append(elem); + builder.append('"'); + } + builder.append("]"); + if (!propGroupSet.isEmpty()) { + builder.append(", propGroupSet: ["); + first = true; + for (String elem : propGroupSet) { + if (first) { + first = false; + } else { + builder.append(", "); + } + builder.append('"'); + builder.append(elem); + builder.append('"'); + } + builder.append("]"); + } + if (propValue_vector != null && propValue_vector.size() > 1) { + builder.append(", propValue_vector size: "); + builder.append(propValue_vector.size()); + } + if (propValue_bytes != null) { + builder.append(", propValue_bytes size: "); + builder.append(propValue_bytes.length); + } + builder.append(", propValue: \""); + builder.append(propValue); + builder.append("\""); + return builder.toString(); + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifier.java b/vcard/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifier.java new file mode 100644 index 000000000..de33a36a4 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifier.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.tests.test_utils; + +import android.test.AndroidTestCase; + +import com.android.vcard.VCardConfig; +import com.android.vcard.VCardParser; +import com.android.vcard.VCardParser_V21; +import com.android.vcard.VCardParser_V30; +import com.android.vcard.exception.VCardException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class PropertyNodesVerifier extends VNodeBuilder { + private final List mPropertyNodesVerifierElemList; + private final AndroidTestCase mAndroidTestCase; + private int mIndex; + + public PropertyNodesVerifier(AndroidTestCase testCase) { + super(); + mPropertyNodesVerifierElemList = new ArrayList(); + mAndroidTestCase = testCase; + } + + public PropertyNodesVerifierElem addPropertyNodesVerifierElem() { + PropertyNodesVerifierElem elem = new PropertyNodesVerifierElem(mAndroidTestCase); + mPropertyNodesVerifierElemList.add(elem); + return elem; + } + + public void verify(int resId, int vCardType) + throws IOException, VCardException { + verify(mAndroidTestCase.getContext().getResources().openRawResource(resId), vCardType); + } + + public void verify(int resId, int vCardType, final VCardParser vCardParser) + throws IOException, VCardException { + verify(mAndroidTestCase.getContext().getResources().openRawResource(resId), + vCardType, vCardParser); + } + + public void verify(InputStream is, int vCardType) throws IOException, VCardException { + final VCardParser vCardParser; + if (VCardConfig.isV30(vCardType)) { + vCardParser = new VCardParser_V30(); + } else { + vCardParser = new VCardParser_V21(); + } + verify(is, vCardType, vCardParser); + } + + public void verify(InputStream is, int vCardType, final VCardParser vCardParser) + throws IOException, VCardException { + try { + vCardParser.parse(is, this); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void endEntry() { + super.endEntry(); + mAndroidTestCase.assertTrue(mIndex < mPropertyNodesVerifierElemList.size()); + mAndroidTestCase.assertTrue(mIndex < vNodeList.size()); + mPropertyNodesVerifierElemList.get(mIndex).verify(vNodeList.get(mIndex)); + mIndex++; + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.java b/vcard/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.java new file mode 100644 index 000000000..6eb84983e --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.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.tests.test_utils; + +import android.content.ContentValues; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +/** + * Utility class which verifies input VNode. + * + * This class first checks whether each propertyNode in the VNode is in the + * "ordered expected property list". + * If the node does not exist in the "ordered list", the class refers to + * "unorderd expected property set" and checks the node is expected somewhere. + */ +public class PropertyNodesVerifierElem { + public static class TypeSet extends HashSet { + public TypeSet(String ... array) { + super(Arrays.asList(array)); + } + } + + public static class GroupSet extends HashSet { + public GroupSet(String ... array) { + super(Arrays.asList(array)); + } + } + + private final HashMap> mOrderedNodeMap; + // Intentionally use ArrayList instead of Set, assuming there may be more than one + // exactly same objects. + private final ArrayList mUnorderedNodeList; + private final TestCase mTestCase; + + public PropertyNodesVerifierElem(TestCase testCase) { + mOrderedNodeMap = new HashMap>(); + mUnorderedNodeList = new ArrayList(); + mTestCase = testCase; + } + + // WithOrder + + public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue) { + return addExpectedNodeWithOrder(propName, propValue, null, null, null, null, null); + } + + public PropertyNodesVerifierElem addExpectedNodeWithOrder( + String propName, String propValue, ContentValues contentValues) { + return addExpectedNodeWithOrder(propName, propValue, null, + null, contentValues, null, null); + } + + public PropertyNodesVerifierElem addExpectedNodeWithOrder( + String propName, List propValueList, ContentValues contentValues) { + return addExpectedNodeWithOrder(propName, null, propValueList, + null, contentValues, null, null); + } + + public PropertyNodesVerifierElem addExpectedNodeWithOrder( + String propName, String propValue, List propValueList) { + return addExpectedNodeWithOrder(propName, propValue, propValueList, null, + null, null, null); + } + + public PropertyNodesVerifierElem addExpectedNodeWithOrder( + String propName, List propValueList) { + final String propValue = concatinateListWithSemiColon(propValueList); + return addExpectedNodeWithOrder(propName, propValue.toString(), propValueList, + null, null, null, null); + } + + public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue, + TypeSet paramMap_TYPE) { + return addExpectedNodeWithOrder(propName, propValue, null, + null, null, paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, + List propValueList, TypeSet paramMap_TYPE) { + return addExpectedNodeWithOrder(propName, null, propValueList, null, null, + paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue, + ContentValues paramMap, TypeSet paramMap_TYPE) { + return addExpectedNodeWithOrder(propName, propValue, null, null, + paramMap, paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue, + List propValueList, TypeSet paramMap_TYPE) { + return addExpectedNodeWithOrder(propName, propValue, propValueList, null, null, + paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue, + List propValueList, byte[] propValue_bytes, + ContentValues paramMap, TypeSet paramMap_TYPE, GroupSet propGroupSet) { + if (propValue == null && propValueList != null) { + propValue = concatinateListWithSemiColon(propValueList); + } + PropertyNode propertyNode = new PropertyNode(propName, + propValue, propValueList, propValue_bytes, + paramMap, paramMap_TYPE, propGroupSet); + List expectedNodeList = mOrderedNodeMap.get(propName); + if (expectedNodeList == null) { + expectedNodeList = new ArrayList(); + mOrderedNodeMap.put(propName, expectedNodeList); + } + expectedNodeList.add(propertyNode); + return this; + } + + // WithoutOrder + + public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue) { + return addExpectedNode(propName, propValue, null, null, null, null, null); + } + + public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, + ContentValues contentValues) { + return addExpectedNode(propName, propValue, null, null, contentValues, null, null); + } + + public PropertyNodesVerifierElem addExpectedNode(String propName, + List propValueList, ContentValues contentValues) { + return addExpectedNode(propName, null, + propValueList, null, contentValues, null, null); + } + + public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, + List propValueList) { + return addExpectedNode(propName, propValue, propValueList, null, null, null, null); + } + + public PropertyNodesVerifierElem addExpectedNode(String propName, + List propValueList) { + return addExpectedNode(propName, null, propValueList, + null, null, null, null); + } + + public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, + TypeSet paramMap_TYPE) { + return addExpectedNode(propName, propValue, null, null, null, paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addExpectedNode(String propName, + List propValueList, TypeSet paramMap_TYPE) { + final String propValue = concatinateListWithSemiColon(propValueList); + return addExpectedNode(propName, propValue, propValueList, null, null, + paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, + List propValueList, TypeSet paramMap_TYPE) { + return addExpectedNode(propName, propValue, propValueList, null, null, + paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, + ContentValues paramMap, TypeSet paramMap_TYPE) { + return addExpectedNode(propName, propValue, null, null, + paramMap, paramMap_TYPE, null); + } + + public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, + List propValueList, byte[] propValue_bytes, + ContentValues paramMap, TypeSet paramMap_TYPE, GroupSet propGroupSet) { + if (propValue == null && propValueList != null) { + propValue = concatinateListWithSemiColon(propValueList); + } + mUnorderedNodeList.add(new PropertyNode(propName, propValue, + propValueList, propValue_bytes, paramMap, paramMap_TYPE, propGroupSet)); + return this; + } + + public void verify(VNode vnode) { + for (PropertyNode actualNode : vnode.propList) { + verifyNode(actualNode.propName, actualNode); + } + if (!mOrderedNodeMap.isEmpty() || !mUnorderedNodeList.isEmpty()) { + List expectedProps = new ArrayList(); + for (List nodes : mOrderedNodeMap.values()) { + for (PropertyNode node : nodes) { + if (!expectedProps.contains(node.propName)) { + expectedProps.add(node.propName); + } + } + } + for (PropertyNode node : mUnorderedNodeList) { + if (!expectedProps.contains(node.propName)) { + expectedProps.add(node.propName); + } + } + mTestCase.fail("Expected property " + Arrays.toString(expectedProps.toArray()) + + " was not found."); + } + } + + private void verifyNode(final String propName, final PropertyNode actualNode) { + List expectedNodeList = mOrderedNodeMap.get(propName); + final int size = (expectedNodeList != null ? expectedNodeList.size() : 0); + if (size > 0) { + for (int i = 0; i < size; i++) { + PropertyNode expectedNode = expectedNodeList.get(i); + List expectedButDifferentValueList = new ArrayList(); + if (expectedNode.propName.equals(propName)) { + if (expectedNode.equals(actualNode)) { + expectedNodeList.remove(i); + if (expectedNodeList.size() == 0) { + mOrderedNodeMap.remove(propName); + } + return; + } else { + expectedButDifferentValueList.add(expectedNode); + } + } + + // "actualNode" is not in ordered expected list. + // Try looking over unordered expected list. + if (tryFoundExpectedNodeFromUnorderedList(actualNode, + expectedButDifferentValueList)) { + return; + } + + if (!expectedButDifferentValueList.isEmpty()) { + // Same propName exists but with different value(s). + failWithExpectedNodeList(propName, actualNode, + expectedButDifferentValueList); + } else { + // There's no expected node with same propName. + mTestCase.fail("Unexpected property \"" + propName + "\" exists."); + } + } + } else { + List expectedButDifferentValueList = + new ArrayList(); + if (tryFoundExpectedNodeFromUnorderedList(actualNode, expectedButDifferentValueList)) { + return; + } else { + if (!expectedButDifferentValueList.isEmpty()) { + // Same propName exists but with different value(s). + failWithExpectedNodeList(propName, actualNode, + expectedButDifferentValueList); + } else { + // There's no expected node with same propName. + mTestCase.fail("Unexpected property \"" + propName + "\" exists."); + } + } + } + } + + private String concatinateListWithSemiColon(List array) { + StringBuffer buffer = new StringBuffer(); + boolean first = true; + for (String propValueElem : array) { + if (first) { + first = false; + } else { + buffer.append(';'); + } + buffer.append(propValueElem); + } + + return buffer.toString(); + } + + private boolean tryFoundExpectedNodeFromUnorderedList(PropertyNode actualNode, + List expectedButDifferentValueList) { + final String propName = actualNode.propName; + int unorderedListSize = mUnorderedNodeList.size(); + for (int i = 0; i < unorderedListSize; i++) { + PropertyNode unorderedExpectedNode = mUnorderedNodeList.get(i); + if (unorderedExpectedNode.propName.equals(propName)) { + if (unorderedExpectedNode.equals(actualNode)) { + mUnorderedNodeList.remove(i); + return true; + } + expectedButDifferentValueList.add(unorderedExpectedNode); + } + } + return false; + } + + private void failWithExpectedNodeList(String propName, PropertyNode actualNode, + List expectedNodeList) { + StringBuilder builder = new StringBuilder(); + for (PropertyNode expectedNode : expectedNodeList) { + builder.append("expected: "); + builder.append(expectedNode.toString()); + builder.append("\n"); + } + mTestCase.fail("Property \"" + propName + "\" has wrong value.\n" + + builder.toString() + + " actual: " + actualNode.toString()); + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java b/vcard/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java new file mode 100644 index 000000000..87d82d202 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java @@ -0,0 +1,339 @@ +/* + * 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.tests.test_utils; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.EntityIterator; +import android.net.Uri; +import android.test.AndroidTestCase; +import android.test.mock.MockContext; +import android.text.TextUtils; +import android.util.Log; + +import com.android.vcard.VCardComposer; +import com.android.vcard.VCardConfig; +import com.android.vcard.VCardEntryConstructor; +import com.android.vcard.VCardInterpreter; +import com.android.vcard.VCardInterpreterCollection; +import com.android.vcard.VCardParser; +import com.android.vcard.VCardParser_V21; +import com.android.vcard.VCardParser_V30; +import com.android.vcard.exception.VCardException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + *

+ * The class lets users checks that given expected vCard data are same as given actual vCard data. + * Able to verify both vCard importer/exporter. + *

+ *

+ * First a user has to initialize the object by calling either + * {@link #initForImportTest(int, int)} or {@link #initForExportTest(int)}. + * "Round trip test" (import -> export -> import, or export -> import -> export) is not supported. + *

+ */ +public class VCardVerifier { + private static final String LOG_TAG = "VCardVerifier"; + + private static class CustomMockContext extends MockContext { + final ContentResolver mResolver; + public CustomMockContext(ContentResolver resolver) { + mResolver = resolver; + } + + @Override + public ContentResolver getContentResolver() { + return mResolver; + } + } + + private class VCardVerifierInternal implements VCardComposer.OneEntryHandler { + public boolean onInit(Context context) { + return true; + } + public boolean onEntryCreated(String vcard) { + verifyOneVCard(vcard); + return true; + } + public void onTerminate() { + } + } + + private final AndroidTestCase mTestCase; + private final VCardVerifierInternal mVCardVerifierInternal; + private int mVCardType; + private boolean mIsV30; + private boolean mIsDoCoMo; + + // Only one of them must be non-empty. + private ExportTestResolver mExportTestResolver; + private InputStream mInputStream; + + // To allow duplication, use list instead of set. + // When null, we don't need to do the verification. + private PropertyNodesVerifier mPropertyNodesVerifier; + private LineVerifier mLineVerifier; + private ContentValuesVerifier mContentValuesVerifier; + private boolean mInitialized; + private boolean mVerified = false; + private String mCharset; + + // Called by VCardTestsBase + public VCardVerifier(AndroidTestCase testCase) { + mTestCase = testCase; + mVCardVerifierInternal = new VCardVerifierInternal(); + mExportTestResolver = null; + mInputStream = null; + mInitialized = false; + mVerified = false; + } + + // Should be called at the beginning of each import test. + public void initForImportTest(int vcardType, int resId) { + if (mInitialized) { + mTestCase.fail("Already initialized"); + } + mVCardType = vcardType; + mIsV30 = VCardConfig.isV30(vcardType); + mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); + setInputResourceId(resId); + mInitialized = true; + } + + // Should be called at the beginning of each export test. + public void initForExportTest(int vcardType) { + initForExportTest(vcardType, "UTF-8"); + } + + public void initForExportTest(int vcardType, String charset) { + if (mInitialized) { + mTestCase.fail("Already initialized"); + } + mExportTestResolver = new ExportTestResolver(mTestCase); + mVCardType = vcardType; + mIsV30 = VCardConfig.isV30(vcardType); + mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); + mInitialized = true; + if (TextUtils.isEmpty(charset)) { + mCharset = "UTF-8"; + } else { + mCharset = charset; + } + } + + private void setInputResourceId(int resId) { + InputStream inputStream = mTestCase.getContext().getResources().openRawResource(resId); + if (inputStream == null) { + mTestCase.fail("Wrong resId: " + resId); + } + setInputStream(inputStream); + } + + private void setInputStream(InputStream inputStream) { + if (mExportTestResolver != null) { + mTestCase.fail("addInputEntry() is called."); + } else if (mInputStream != null) { + mTestCase.fail("InputStream is already set"); + } + mInputStream = inputStream; + } + + public ContactEntry addInputEntry() { + if (!mInitialized) { + mTestCase.fail("Not initialized"); + } + if (mInputStream != null) { + mTestCase.fail("setInputStream is called"); + } + return mExportTestResolver.addInputContactEntry(); + } + + public PropertyNodesVerifierElem addPropertyNodesVerifierElem() { + if (!mInitialized) { + mTestCase.fail("Not initialized"); + } + if (mPropertyNodesVerifier == null) { + mPropertyNodesVerifier = new PropertyNodesVerifier(mTestCase); + } + PropertyNodesVerifierElem elem = + mPropertyNodesVerifier.addPropertyNodesVerifierElem(); + elem.addExpectedNodeWithOrder("VERSION", (mIsV30 ? "3.0" : "2.1")); + + return elem; + } + + public PropertyNodesVerifierElem addPropertyNodesVerifierElemWithEmptyName() { + if (!mInitialized) { + mTestCase.fail("Not initialized"); + } + PropertyNodesVerifierElem elem = addPropertyNodesVerifierElem(); + if (mIsV30) { + elem.addExpectedNodeWithOrder("N", "").addExpectedNodeWithOrder("FN", ""); + } else if (mIsDoCoMo) { + elem.addExpectedNodeWithOrder("N", ""); + } + return elem; + } + + public LineVerifierElem addLineVerifierElem() { + if (!mInitialized) { + mTestCase.fail("Not initialized"); + } + if (mLineVerifier == null) { + mLineVerifier = new LineVerifier(mTestCase, mVCardType); + } + return mLineVerifier.addLineVerifierElem(); + } + + public ContentValuesVerifierElem addContentValuesVerifierElem() { + if (!mInitialized) { + mTestCase.fail("Not initialized"); + } + if (mContentValuesVerifier == null) { + mContentValuesVerifier = new ContentValuesVerifier(); + } + + return mContentValuesVerifier.addElem(mTestCase); + } + + private void verifyOneVCard(final String vcard) { + Log.d(LOG_TAG, vcard); + final VCardInterpreter builder; + if (mContentValuesVerifier != null) { + final VNodeBuilder vnodeBuilder = mPropertyNodesVerifier; + final VCardEntryConstructor vcardDataBuilder = + new VCardEntryConstructor(mVCardType); + vcardDataBuilder.addEntryHandler(mContentValuesVerifier); + if (mPropertyNodesVerifier != null) { + builder = new VCardInterpreterCollection(Arrays.asList( + mPropertyNodesVerifier, vcardDataBuilder)); + } else { + builder = vnodeBuilder; + } + } else { + if (mPropertyNodesVerifier != null) { + builder = mPropertyNodesVerifier; + } else { + return; + } + } + + InputStream is = null; + try { + // Note: we must not specify charset toward vCard parsers. This code checks whether + // those parsers are able to encode given binary without any extra information for + // charset. + final VCardParser parser = (mIsV30 ? + new VCardParser_V30(mVCardType) : new VCardParser_V21(mVCardType)); + is = new ByteArrayInputStream(vcard.getBytes(mCharset)); + parser.parse(is, builder); + } catch (IOException e) { + mTestCase.fail("Unexpected IOException: " + e.getMessage()); + } catch (VCardException e) { + mTestCase.fail("Unexpected VCardException: " + e.getMessage()); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + mTestCase.fail("Unexpected IOException: " + e.getMessage()); + } + } + } + } + + public void verify() { + if (!mInitialized) { + mTestCase.fail("Not initialized."); + } + if (mVerified) { + mTestCase.fail("verify() was called twice."); + } + if (mInputStream != null) { + try { + verifyForImportTest(); + } catch (IOException e) { + mTestCase.fail("IOException was thrown: " + e.getMessage()); + } catch (VCardException e) { + mTestCase.fail("VCardException was thrown: " + e.getMessage()); + } + } else if (mExportTestResolver != null){ + verifyForExportTest(); + } else { + mTestCase.fail("No input is determined"); + } + mVerified = true; + } + + private void verifyForImportTest() throws IOException, VCardException { + if (mLineVerifier != null) { + mTestCase.fail("Not supported now."); + } + if (mContentValuesVerifier != null) { + mContentValuesVerifier.verify(mInputStream, mVCardType); + } + } + + public static EntityIterator mockGetEntityIteratorMethod( + final ContentResolver resolver, + final Uri uri, final String selection, + final String[] selectionArgs, final String sortOrder) { + if (ExportTestResolver.class.equals(resolver.getClass())) { + return ((ExportTestResolver)resolver).getProvider().queryEntities( + uri, selection, selectionArgs, sortOrder); + } + + Log.e(LOG_TAG, "Unexpected provider given."); + return null; + } + + private Method getMockGetEntityIteratorMethod() + throws SecurityException, NoSuchMethodException { + return this.getClass().getMethod("mockGetEntityIteratorMethod", + ContentResolver.class, Uri.class, String.class, String[].class, String.class); + } + + private void verifyForExportTest() { + final VCardComposer composer = + new VCardComposer(new CustomMockContext(mExportTestResolver), mVCardType, mCharset); + composer.addHandler(mLineVerifier); + composer.addHandler(mVCardVerifierInternal); + if (!composer.init(VCardComposer.CONTACTS_TEST_CONTENT_URI, null, null, null)) { + mTestCase.fail("init() failed. Reason: " + composer.getErrorReason()); + } + mTestCase.assertFalse(composer.isAfterLast()); + try { + while (!composer.isAfterLast()) { + try { + final Method mockGetEntityIteratorMethod = getMockGetEntityIteratorMethod(); + mTestCase.assertNotNull(mockGetEntityIteratorMethod); + mTestCase.assertTrue(composer.createOneEntry(mockGetEntityIteratorMethod)); + } catch (Exception e) { + e.printStackTrace(); + mTestCase.fail(); + } + } + } finally { + composer.terminate(); + } + } +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/VNode.java b/vcard/tests/src/com/android/vcard/tests/test_utils/VNode.java new file mode 100644 index 000000000..2ca762b0a --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/VNode.java @@ -0,0 +1,30 @@ +/* + * 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.tests.test_utils; + +import java.util.ArrayList; + +/** + * Previously used in main vCard handling code but now exists only for testing. + */ +public class VNode { + public String VName; + + public ArrayList propList = new ArrayList(); + + /** 0:parse over. 1:parsing. */ + public int parseStatus = 1; +} diff --git a/vcard/tests/src/com/android/vcard/tests/test_utils/VNodeBuilder.java b/vcard/tests/src/com/android/vcard/tests/test_utils/VNodeBuilder.java new file mode 100644 index 000000000..9b4fe8358 --- /dev/null +++ b/vcard/tests/src/com/android/vcard/tests/test_utils/VNodeBuilder.java @@ -0,0 +1,247 @@ +/* + * 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.tests.test_utils; + +import android.content.ContentValues; +import android.util.Base64; +import android.util.CharsetUtils; +import android.util.Log; + +import com.android.vcard.VCardConfig; +import com.android.vcard.VCardInterpreter; +import com.android.vcard.VCardUtils; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * The class storing the parse result to custom datastruct: + * {@link VNode}, and {@link PropertyNode}. + * Maybe several vcard instance, so use vNodeList to store. + *

+ *

+ * This is called VNode, not VCardNode, since it was used for expressing vCalendar (iCal). + *

+ */ +/* package */ class VNodeBuilder implements VCardInterpreter { + static private String LOG_TAG = "VNodeBuilder"; + + public List vNodeList = new ArrayList(); + private int mNodeListPos = 0; + private VNode mCurrentVNode; + private PropertyNode mCurrentPropNode; + private String mCurrentParamType; + + /** + * The charset using which VParser parses the text. + */ + private String mSourceCharset; + + /** + * The charset with which byte array is encoded to String. + */ + private String mTargetCharset; + + private boolean mStrictLineBreakParsing; + + public VNodeBuilder() { + this(VCardConfig.DEFAULT_INTERMEDIATE_CHARSET, VCardConfig.DEFAULT_IMPORT_CHARSET, false); + } + + public VNodeBuilder(String targetCharset, boolean strictLineBreakParsing) { + this(null, targetCharset, strictLineBreakParsing); + } + + /** + * @hide sourceCharset is temporal. + */ + public VNodeBuilder(String sourceCharset, String targetCharset, + boolean strictLineBreakParsing) { + if (sourceCharset != null) { + mSourceCharset = sourceCharset; + } else { + mSourceCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET; + } + if (targetCharset != null) { + mTargetCharset = targetCharset; + } else { + mTargetCharset = VCardConfig.DEFAULT_IMPORT_CHARSET; + } + mStrictLineBreakParsing = strictLineBreakParsing; + } + + public void start() { + } + + public void end() { + } + + // Note: I guess that this code assumes the Record may nest like this: + // START:VPOS + // ... + // START:VPOS2 + // ... + // END:VPOS2 + // ... + // END:VPOS + // + // However the following code has a bug. + // When error occurs after calling startRecord(), the entry which is probably + // the cause of the error remains to be in vNodeList, while endRecord() is not called. + // + // I leave this code as is since I'm not familiar with vcalendar specification. + // But I believe we should refactor this code in the future. + // Until this, the last entry has to be removed when some error occurs. + public void startEntry() { + VNode vnode = new VNode(); + vnode.parseStatus = 1; + vnode.VName = "VCARD"; + // I feel this should be done in endRecord(), but it cannot be done because of + // the reason above. + vNodeList.add(vnode); + mNodeListPos = vNodeList.size() - 1; + mCurrentVNode = vNodeList.get(mNodeListPos); + } + + public void endEntry() { + VNode endNode = vNodeList.get(mNodeListPos); + endNode.parseStatus = 0; + while(mNodeListPos > 0){ + mNodeListPos--; + if((vNodeList.get(mNodeListPos)).parseStatus == 1) + break; + } + mCurrentVNode = vNodeList.get(mNodeListPos); + } + + public void startProperty() { + mCurrentPropNode = new PropertyNode(); + } + + public void endProperty() { + mCurrentVNode.propList.add(mCurrentPropNode); + } + + public void propertyName(String name) { + mCurrentPropNode.propName = name; + } + + public void propertyGroup(String group) { + mCurrentPropNode.propGroupSet.add(group); + } + + public void propertyParamType(String type) { + mCurrentParamType = type; + } + + public void propertyParamValue(String value) { + if (mCurrentParamType == null || + mCurrentParamType.equalsIgnoreCase("TYPE")) { + mCurrentPropNode.paramMap_TYPE.add(value); + } else { + mCurrentPropNode.paramMap.put(mCurrentParamType, value); + } + + mCurrentParamType = null; + } + + private String encodeString(String originalString, String targetCharset) { + if (mSourceCharset.equalsIgnoreCase(targetCharset)) { + return originalString; + } + Charset charset = Charset.forName(mSourceCharset); + ByteBuffer byteBuffer = charset.encode(originalString); + // byteBuffer.array() "may" return byte array which is larger than + // byteBuffer.remaining(). Here, we keep on the safe side. + byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + try { + return new String(bytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return null; + } + } + + private String handleOneValue(String value, String targetCharset, String encoding) { + if (encoding != null) { + encoding = encoding.toUpperCase(); + if (encoding.equals("BASE64") || encoding.equals("B")) { + // Assume BASE64 is used only when the number of values is 1. + mCurrentPropNode.propValue_bytes = Base64.decode(value.getBytes(), Base64.NO_WRAP); + return value; + } else if (encoding.equals("QUOTED-PRINTABLE")) { + return VCardUtils.parseQuotedPrintable( + value, mStrictLineBreakParsing, mSourceCharset, targetCharset); + } + // Unknown encoding. Fall back to default. + } + return encodeString(value, targetCharset); + } + + public void propertyValues(List values) { + if (values == null || values.size() == 0) { + mCurrentPropNode.propValue_bytes = null; + mCurrentPropNode.propValue_vector.clear(); + mCurrentPropNode.propValue_vector.add(""); + mCurrentPropNode.propValue = ""; + return; + } + + ContentValues paramMap = mCurrentPropNode.paramMap; + + String targetCharset = CharsetUtils.nameForDefaultVendor(paramMap.getAsString("CHARSET")); + String encoding = paramMap.getAsString("ENCODING"); + + if (targetCharset == null || targetCharset.length() == 0) { + targetCharset = mTargetCharset; + } + + for (String value : values) { + mCurrentPropNode.propValue_vector.add( + handleOneValue(value, targetCharset, encoding)); + } + + mCurrentPropNode.propValue = listToString(mCurrentPropNode.propValue_vector); + } + + private String listToString(List list){ + int size = list.size(); + if (size > 1) { + StringBuilder typeListB = new StringBuilder(); + for (String type : list) { + typeListB.append(type).append(";"); + } + int len = typeListB.length(); + if (len > 0 && typeListB.charAt(len - 1) == ';') { + return typeListB.substring(0, len - 1); + } + return typeListB.toString(); + } else if (size == 1) { + return list.get(0); + } else { + return ""; + } + } + + public String getResult(){ + throw new RuntimeException("Not supported"); + } +}