Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2009 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| 5 | * use this file except in compliance with the License. You may obtain a copy of |
| 6 | * the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 13 | * License for the specific language governing permissions and limitations under |
| 14 | * the License. |
| 15 | */ |
| 16 | package android.pim.vcard; |
| 17 | |
| 18 | import android.content.ContentValues; |
| 19 | import android.provider.ContactsContract.CommonDataKinds.Email; |
| 20 | import android.provider.ContactsContract.CommonDataKinds.Event; |
| 21 | import android.provider.ContactsContract.CommonDataKinds.Im; |
| 22 | import android.provider.ContactsContract.CommonDataKinds.Nickname; |
| 23 | import android.provider.ContactsContract.CommonDataKinds.Note; |
| 24 | import android.provider.ContactsContract.CommonDataKinds.Organization; |
| 25 | import android.provider.ContactsContract.CommonDataKinds.Phone; |
| 26 | import android.provider.ContactsContract.CommonDataKinds.Photo; |
| 27 | import android.provider.ContactsContract.CommonDataKinds.Relation; |
| 28 | import android.provider.ContactsContract.CommonDataKinds.StructuredName; |
| 29 | import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; |
| 30 | import android.provider.ContactsContract.CommonDataKinds.Website; |
| 31 | import android.telephony.PhoneNumberUtils; |
| 32 | import android.text.TextUtils; |
| 33 | import android.util.CharsetUtils; |
| 34 | import android.util.Log; |
| 35 | |
| 36 | import org.apache.commons.codec.binary.Base64; |
| 37 | |
| 38 | import java.io.UnsupportedEncodingException; |
| 39 | import java.nio.charset.UnsupportedCharsetException; |
| 40 | import java.util.ArrayList; |
| 41 | import java.util.Arrays; |
| 42 | import java.util.Collections; |
| 43 | import java.util.HashMap; |
| 44 | import java.util.HashSet; |
| 45 | import java.util.List; |
| 46 | import java.util.Map; |
| 47 | import java.util.Set; |
| 48 | |
| 49 | /** |
| 50 | * The class which lets users create their own vCard String. |
| 51 | */ |
| 52 | public class VCardBuilder { |
| 53 | private static final String LOG_TAG = "VCardBuilder"; |
| 54 | |
| 55 | // If you add the other element, please check all the columns are able to be |
| 56 | // converted to String. |
| 57 | // |
| 58 | // e.g. BLOB is not what we can handle here now. |
| 59 | private static final Set<String> sAllowedAndroidPropertySet = |
| 60 | Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( |
| 61 | Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, |
| 62 | Relation.CONTENT_ITEM_TYPE))); |
| 63 | |
| 64 | public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; |
| 65 | public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; |
| 66 | public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; |
| 67 | |
| 68 | private static final String VCARD_DATA_VCARD = "VCARD"; |
| 69 | private static final String VCARD_DATA_PUBLIC = "PUBLIC"; |
| 70 | |
| 71 | private static final String VCARD_PARAM_SEPARATOR = ";"; |
| 72 | private static final String VCARD_END_OF_LINE = "\r\n"; |
| 73 | private static final String VCARD_DATA_SEPARATOR = ":"; |
| 74 | private static final String VCARD_ITEM_SEPARATOR = ";"; |
| 75 | private static final String VCARD_WS = " "; |
| 76 | private static final String VCARD_PARAM_EQUAL = "="; |
| 77 | |
| 78 | private static final String VCARD_PARAM_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; |
| 79 | |
| 80 | private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64"; |
| 81 | private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b"; |
| 82 | |
| 83 | private static final String SHIFT_JIS = "SHIFT_JIS"; |
| 84 | private static final String UTF_8 = "UTF-8"; |
| 85 | |
| 86 | private final int mVCardType; |
| 87 | |
| 88 | private final boolean mIsV30; |
| 89 | private final boolean mIsJapaneseMobilePhone; |
| 90 | private final boolean mOnlyOneNoteFieldIsAvailable; |
| 91 | private final boolean mIsDoCoMo; |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 92 | private final boolean mShouldUseQuotedPrintable; |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 93 | private final boolean mUsesAndroidProperty; |
| 94 | private final boolean mUsesDefactProperty; |
| 95 | private final boolean mUsesUtf8; |
| 96 | private final boolean mUsesShiftJis; |
| 97 | private final boolean mAppendTypeParamName; |
Daisuke Miyakawa | 49c0dec | 2009-11-18 17:11:43 +0900 | [diff] [blame] | 98 | private final boolean mRefrainsQPToNameProperties; |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 99 | private final boolean mNeedsToConvertPhoneticString; |
| 100 | |
| 101 | private final boolean mShouldAppendCharsetParam; |
| 102 | |
| 103 | private final String mCharsetString; |
| 104 | private final String mVCardCharsetParameter; |
| 105 | |
| 106 | private StringBuilder mBuilder; |
| 107 | private boolean mEndAppended; |
| 108 | |
| 109 | public VCardBuilder(final int vcardType) { |
| 110 | mVCardType = vcardType; |
| 111 | |
| 112 | mIsV30 = VCardConfig.isV30(vcardType); |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 113 | mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 114 | mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); |
| 115 | mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType); |
| 116 | mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType); |
| 117 | mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType); |
| 118 | mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); |
| 119 | mUsesUtf8 = VCardConfig.usesUtf8(vcardType); |
| 120 | mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); |
Daisuke Miyakawa | 49c0dec | 2009-11-18 17:11:43 +0900 | [diff] [blame] | 121 | mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 122 | mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); |
| 123 | mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); |
| 124 | |
| 125 | mShouldAppendCharsetParam = !(mIsV30 && mUsesUtf8); |
| 126 | |
| 127 | if (mIsDoCoMo) { |
| 128 | String charset; |
| 129 | try { |
| 130 | charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); |
| 131 | } catch (UnsupportedCharsetException e) { |
| 132 | Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); |
| 133 | charset = SHIFT_JIS; |
| 134 | } |
| 135 | mCharsetString = charset; |
| 136 | // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but |
| 137 | // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in |
| 138 | // Android, not shown to the public). |
| 139 | mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; |
| 140 | } else if (mUsesShiftJis) { |
| 141 | String charset; |
| 142 | try { |
| 143 | charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); |
| 144 | } catch (UnsupportedCharsetException e) { |
| 145 | Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); |
| 146 | charset = SHIFT_JIS; |
| 147 | } |
| 148 | mCharsetString = charset; |
| 149 | mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; |
| 150 | } else { |
| 151 | mCharsetString = UTF_8; |
| 152 | mVCardCharsetParameter = "CHARSET=" + UTF_8; |
| 153 | } |
| 154 | clear(); |
| 155 | } |
| 156 | |
| 157 | public void clear() { |
| 158 | mBuilder = new StringBuilder(); |
| 159 | mEndAppended = false; |
| 160 | appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD); |
| 161 | if (mIsV30) { |
| 162 | appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30); |
| 163 | } else { |
| 164 | appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21); |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | private boolean containsNonEmptyName(final ContentValues contentValues) { |
| 169 | final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); |
| 170 | final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); |
| 171 | final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); |
| 172 | final String prefix = contentValues.getAsString(StructuredName.PREFIX); |
| 173 | final String suffix = contentValues.getAsString(StructuredName.SUFFIX); |
| 174 | final String phoneticFamilyName = |
| 175 | contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); |
| 176 | final String phoneticMiddleName = |
| 177 | contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); |
| 178 | final String phoneticGivenName = |
| 179 | contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); |
| 180 | final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); |
| 181 | return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) && |
| 182 | TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) && |
| 183 | TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) && |
| 184 | TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) && |
| 185 | TextUtils.isEmpty(displayName)); |
| 186 | } |
| 187 | |
| 188 | private ContentValues getPrimaryContentValue(final List<ContentValues> contentValuesList) { |
| 189 | ContentValues primaryContentValues = null; |
| 190 | ContentValues subprimaryContentValues = null; |
| 191 | for (ContentValues contentValues : contentValuesList) { |
| 192 | if (contentValues == null){ |
| 193 | continue; |
| 194 | } |
| 195 | Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY); |
| 196 | if (isSuperPrimary != null && isSuperPrimary > 0) { |
| 197 | // We choose "super primary" ContentValues. |
| 198 | primaryContentValues = contentValues; |
| 199 | break; |
| 200 | } else if (primaryContentValues == null) { |
| 201 | // We choose the first "primary" ContentValues |
| 202 | // if "super primary" ContentValues does not exist. |
| 203 | final Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY); |
| 204 | if (isPrimary != null && isPrimary > 0 && |
| 205 | containsNonEmptyName(contentValues)) { |
| 206 | primaryContentValues = contentValues; |
| 207 | // Do not break, since there may be ContentValues with "super primary" |
| 208 | // afterword. |
| 209 | } else if (subprimaryContentValues == null && |
| 210 | containsNonEmptyName(contentValues)) { |
| 211 | subprimaryContentValues = contentValues; |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | if (primaryContentValues == null) { |
| 217 | if (subprimaryContentValues != null) { |
| 218 | // We choose the first ContentValues if any "primary" ContentValues does not exist. |
| 219 | primaryContentValues = subprimaryContentValues; |
| 220 | } else { |
| 221 | Log.e(LOG_TAG, "All ContentValues given from database is empty."); |
| 222 | primaryContentValues = new ContentValues(); |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | return primaryContentValues; |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * For safety, we'll emit just one value around StructuredName, as external importers |
| 231 | * may get confused with multiple "N", "FN", etc. properties, though it is valid in |
| 232 | * vCard spec. |
| 233 | */ |
| 234 | public VCardBuilder appendNameProperties(final List<ContentValues> contentValuesList) { |
| 235 | if (contentValuesList == null || contentValuesList.isEmpty()) { |
| 236 | if (mIsDoCoMo) { |
| 237 | appendLine(VCardConstants.PROPERTY_N, ""); |
| 238 | } else if (mIsV30) { |
| 239 | // vCard 3.0 requires "N" and "FN" properties. |
| 240 | appendLine(VCardConstants.PROPERTY_N, ""); |
| 241 | appendLine(VCardConstants.PROPERTY_FN, ""); |
| 242 | } |
| 243 | return this; |
| 244 | } |
| 245 | |
| 246 | final ContentValues contentValues = getPrimaryContentValue(contentValuesList); |
| 247 | final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); |
| 248 | final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); |
| 249 | final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); |
| 250 | final String prefix = contentValues.getAsString(StructuredName.PREFIX); |
| 251 | final String suffix = contentValues.getAsString(StructuredName.SUFFIX); |
| 252 | final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); |
| 253 | |
| 254 | if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { |
| 255 | final boolean reallyAppendCharsetParameterToName = |
| 256 | shouldAppendCharsetParam(familyName, givenName, middleName, prefix, suffix); |
| 257 | final boolean reallyUseQuotedPrintableToName = |
Daisuke Miyakawa | 49c0dec | 2009-11-18 17:11:43 +0900 | [diff] [blame] | 258 | (!mRefrainsQPToNameProperties && |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 259 | !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && |
| 260 | VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && |
| 261 | VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && |
| 262 | VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && |
| 263 | VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); |
| 264 | |
| 265 | final String formattedName; |
| 266 | if (!TextUtils.isEmpty(displayName)) { |
| 267 | formattedName = displayName; |
| 268 | } else { |
| 269 | formattedName = VCardUtils.constructNameFromElements( |
| 270 | VCardConfig.getNameOrderType(mVCardType), |
| 271 | familyName, middleName, givenName, prefix, suffix); |
| 272 | } |
| 273 | final boolean reallyAppendCharsetParameterToFN = |
| 274 | shouldAppendCharsetParam(formattedName); |
| 275 | final boolean reallyUseQuotedPrintableToFN = |
Daisuke Miyakawa | 49c0dec | 2009-11-18 17:11:43 +0900 | [diff] [blame] | 276 | !mRefrainsQPToNameProperties && |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 277 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName); |
| 278 | |
| 279 | final String encodedFamily; |
| 280 | final String encodedGiven; |
| 281 | final String encodedMiddle; |
| 282 | final String encodedPrefix; |
| 283 | final String encodedSuffix; |
| 284 | if (reallyUseQuotedPrintableToName) { |
| 285 | encodedFamily = encodeQuotedPrintable(familyName); |
| 286 | encodedGiven = encodeQuotedPrintable(givenName); |
| 287 | encodedMiddle = encodeQuotedPrintable(middleName); |
| 288 | encodedPrefix = encodeQuotedPrintable(prefix); |
| 289 | encodedSuffix = encodeQuotedPrintable(suffix); |
| 290 | } else { |
| 291 | encodedFamily = escapeCharacters(familyName); |
| 292 | encodedGiven = escapeCharacters(givenName); |
| 293 | encodedMiddle = escapeCharacters(middleName); |
| 294 | encodedPrefix = escapeCharacters(prefix); |
| 295 | encodedSuffix = escapeCharacters(suffix); |
| 296 | } |
| 297 | |
| 298 | final String encodedFormattedname = |
| 299 | (reallyUseQuotedPrintableToFN ? |
| 300 | encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName)); |
| 301 | |
| 302 | mBuilder.append(VCardConstants.PROPERTY_N); |
| 303 | if (mIsDoCoMo) { |
| 304 | if (reallyAppendCharsetParameterToName) { |
| 305 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 306 | mBuilder.append(mVCardCharsetParameter); |
| 307 | } |
| 308 | if (reallyUseQuotedPrintableToName) { |
| 309 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 310 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 311 | } |
| 312 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 313 | // DoCoMo phones require that all the elements in the "family name" field. |
| 314 | mBuilder.append(formattedName); |
| 315 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 316 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 317 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 318 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 319 | } else { |
| 320 | if (reallyAppendCharsetParameterToName) { |
| 321 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 322 | mBuilder.append(mVCardCharsetParameter); |
| 323 | } |
| 324 | if (reallyUseQuotedPrintableToName) { |
| 325 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 326 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 327 | } |
| 328 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 329 | mBuilder.append(encodedFamily); |
| 330 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 331 | mBuilder.append(encodedGiven); |
| 332 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 333 | mBuilder.append(encodedMiddle); |
| 334 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 335 | mBuilder.append(encodedPrefix); |
| 336 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 337 | mBuilder.append(encodedSuffix); |
| 338 | } |
| 339 | mBuilder.append(VCARD_END_OF_LINE); |
| 340 | |
| 341 | // FN property |
| 342 | mBuilder.append(VCardConstants.PROPERTY_FN); |
| 343 | if (reallyAppendCharsetParameterToFN) { |
| 344 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 345 | mBuilder.append(mVCardCharsetParameter); |
| 346 | } |
| 347 | if (reallyUseQuotedPrintableToFN) { |
| 348 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 349 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 350 | } |
| 351 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 352 | mBuilder.append(encodedFormattedname); |
| 353 | mBuilder.append(VCARD_END_OF_LINE); |
| 354 | } else if (!TextUtils.isEmpty(displayName)) { |
| 355 | final boolean reallyUseQuotedPrintableToDisplayName = |
Daisuke Miyakawa | 49c0dec | 2009-11-18 17:11:43 +0900 | [diff] [blame] | 356 | (!mRefrainsQPToNameProperties && |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 357 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); |
| 358 | final String encodedDisplayName = |
| 359 | reallyUseQuotedPrintableToDisplayName ? |
| 360 | encodeQuotedPrintable(displayName) : |
| 361 | escapeCharacters(displayName); |
| 362 | |
| 363 | mBuilder.append(VCardConstants.PROPERTY_N); |
| 364 | if (shouldAppendCharsetParam(displayName)) { |
| 365 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 366 | mBuilder.append(mVCardCharsetParameter); |
| 367 | } |
| 368 | if (reallyUseQuotedPrintableToDisplayName) { |
| 369 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 370 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 371 | } |
| 372 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 373 | mBuilder.append(encodedDisplayName); |
| 374 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 375 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 376 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 377 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 378 | mBuilder.append(VCARD_END_OF_LINE); |
| 379 | mBuilder.append(VCardConstants.PROPERTY_FN); |
| 380 | |
| 381 | // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it |
| 382 | // when it would be useful for external importers, assuming no external |
| 383 | // importer allows this vioration. |
| 384 | if (shouldAppendCharsetParam(displayName)) { |
| 385 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 386 | mBuilder.append(mVCardCharsetParameter); |
| 387 | } |
| 388 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 389 | mBuilder.append(encodedDisplayName); |
| 390 | mBuilder.append(VCARD_END_OF_LINE); |
| 391 | } else if (mIsV30) { |
| 392 | // vCard 3.0 specification requires these fields. |
| 393 | appendLine(VCardConstants.PROPERTY_N, ""); |
| 394 | appendLine(VCardConstants.PROPERTY_FN, ""); |
| 395 | } else if (mIsDoCoMo) { |
| 396 | appendLine(VCardConstants.PROPERTY_N, ""); |
| 397 | } |
| 398 | |
| 399 | appendPhoneticNameFields(contentValues); |
| 400 | return this; |
| 401 | } |
| 402 | |
| 403 | private void appendPhoneticNameFields(final ContentValues contentValues) { |
| 404 | final String phoneticFamilyName; |
| 405 | final String phoneticMiddleName; |
| 406 | final String phoneticGivenName; |
| 407 | { |
| 408 | final String tmpPhoneticFamilyName = |
| 409 | contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); |
| 410 | final String tmpPhoneticMiddleName = |
| 411 | contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); |
| 412 | final String tmpPhoneticGivenName = |
| 413 | contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); |
| 414 | if (mNeedsToConvertPhoneticString) { |
| 415 | phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName); |
| 416 | phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName); |
| 417 | phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName); |
| 418 | } else { |
| 419 | phoneticFamilyName = tmpPhoneticFamilyName; |
| 420 | phoneticMiddleName = tmpPhoneticMiddleName; |
| 421 | phoneticGivenName = tmpPhoneticGivenName; |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | if (TextUtils.isEmpty(phoneticFamilyName) |
| 426 | && TextUtils.isEmpty(phoneticMiddleName) |
| 427 | && TextUtils.isEmpty(phoneticGivenName)) { |
| 428 | if (mIsDoCoMo) { |
| 429 | mBuilder.append(VCardConstants.PROPERTY_SOUND); |
| 430 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 431 | mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); |
| 432 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 433 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 434 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 435 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 436 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 437 | mBuilder.append(VCARD_END_OF_LINE); |
| 438 | } |
| 439 | return; |
| 440 | } |
| 441 | |
| 442 | // Try to emit the field(s) related to phonetic name. |
| 443 | if (mIsV30) { |
| 444 | final String sortString = VCardUtils |
| 445 | .constructNameFromElements(mVCardType, |
| 446 | phoneticFamilyName, phoneticMiddleName, phoneticGivenName); |
| 447 | mBuilder.append(VCardConstants.PROPERTY_SORT_STRING); |
| 448 | if (shouldAppendCharsetParam(sortString)) { |
| 449 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 450 | mBuilder.append(mVCardCharsetParameter); |
| 451 | } |
| 452 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 453 | mBuilder.append(escapeCharacters(sortString)); |
| 454 | mBuilder.append(VCARD_END_OF_LINE); |
| 455 | } else if (mIsJapaneseMobilePhone) { |
| 456 | // Note: There is no appropriate property for expressing |
| 457 | // phonetic name in vCard 2.1, while there is in |
| 458 | // vCard 3.0 (SORT-STRING). |
| 459 | // We chose to use DoCoMo's way when the device is Japanese one |
| 460 | // since it is supported by |
| 461 | // a lot of Japanese mobile phones. This is "X-" property, so |
| 462 | // any parser hopefully would not get confused with this. |
| 463 | // |
| 464 | // Also, DoCoMo's specification requires vCard composer to use just the first |
| 465 | // column. |
| 466 | // i.e. |
| 467 | // o SOUND;X-IRMC-N:Miyakawa Daisuke;;;; |
| 468 | // x SOUND;X-IRMC-N:Miyakawa;Daisuke;;; |
| 469 | mBuilder.append(VCardConstants.PROPERTY_SOUND); |
| 470 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 471 | mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); |
| 472 | |
| 473 | boolean reallyUseQuotedPrintable = |
Daisuke Miyakawa | 49c0dec | 2009-11-18 17:11:43 +0900 | [diff] [blame] | 474 | (!mRefrainsQPToNameProperties |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 475 | && !(VCardUtils.containsOnlyNonCrLfPrintableAscii( |
| 476 | phoneticFamilyName) |
| 477 | && VCardUtils.containsOnlyNonCrLfPrintableAscii( |
| 478 | phoneticMiddleName) |
| 479 | && VCardUtils.containsOnlyNonCrLfPrintableAscii( |
| 480 | phoneticGivenName))); |
| 481 | |
| 482 | final String encodedPhoneticFamilyName; |
| 483 | final String encodedPhoneticMiddleName; |
| 484 | final String encodedPhoneticGivenName; |
| 485 | if (reallyUseQuotedPrintable) { |
| 486 | encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); |
| 487 | encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); |
| 488 | encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); |
| 489 | } else { |
| 490 | encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); |
| 491 | encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); |
| 492 | encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); |
| 493 | } |
| 494 | |
| 495 | if (shouldAppendCharsetParam(encodedPhoneticFamilyName, |
| 496 | encodedPhoneticMiddleName, encodedPhoneticGivenName)) { |
| 497 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 498 | mBuilder.append(mVCardCharsetParameter); |
| 499 | } |
| 500 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 501 | { |
| 502 | boolean first = true; |
| 503 | if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) { |
| 504 | mBuilder.append(encodedPhoneticFamilyName); |
| 505 | first = false; |
| 506 | } |
| 507 | if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) { |
| 508 | if (first) { |
| 509 | first = false; |
| 510 | } else { |
| 511 | mBuilder.append(' '); |
| 512 | } |
| 513 | mBuilder.append(encodedPhoneticMiddleName); |
| 514 | } |
| 515 | if (!TextUtils.isEmpty(encodedPhoneticGivenName)) { |
| 516 | if (!first) { |
| 517 | mBuilder.append(' '); |
| 518 | } |
| 519 | mBuilder.append(encodedPhoneticGivenName); |
| 520 | } |
| 521 | } |
| 522 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 523 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 524 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 525 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 526 | mBuilder.append(VCARD_END_OF_LINE); |
| 527 | } |
| 528 | |
| 529 | if (mUsesDefactProperty) { |
| 530 | if (!TextUtils.isEmpty(phoneticGivenName)) { |
| 531 | final boolean reallyUseQuotedPrintable = |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 532 | (mShouldUseQuotedPrintable && |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 533 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); |
| 534 | final String encodedPhoneticGivenName; |
| 535 | if (reallyUseQuotedPrintable) { |
| 536 | encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); |
| 537 | } else { |
| 538 | encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); |
| 539 | } |
| 540 | mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME); |
| 541 | if (shouldAppendCharsetParam(phoneticGivenName)) { |
| 542 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 543 | mBuilder.append(mVCardCharsetParameter); |
| 544 | } |
| 545 | if (reallyUseQuotedPrintable) { |
| 546 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 547 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 548 | } |
| 549 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 550 | mBuilder.append(encodedPhoneticGivenName); |
| 551 | mBuilder.append(VCARD_END_OF_LINE); |
| 552 | } |
| 553 | if (!TextUtils.isEmpty(phoneticMiddleName)) { |
| 554 | final boolean reallyUseQuotedPrintable = |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 555 | (mShouldUseQuotedPrintable && |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 556 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); |
| 557 | final String encodedPhoneticMiddleName; |
| 558 | if (reallyUseQuotedPrintable) { |
| 559 | encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); |
| 560 | } else { |
| 561 | encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); |
| 562 | } |
| 563 | mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME); |
| 564 | if (shouldAppendCharsetParam(phoneticMiddleName)) { |
| 565 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 566 | mBuilder.append(mVCardCharsetParameter); |
| 567 | } |
| 568 | if (reallyUseQuotedPrintable) { |
| 569 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 570 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 571 | } |
| 572 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 573 | mBuilder.append(encodedPhoneticMiddleName); |
| 574 | mBuilder.append(VCARD_END_OF_LINE); |
| 575 | } |
| 576 | if (!TextUtils.isEmpty(phoneticFamilyName)) { |
| 577 | final boolean reallyUseQuotedPrintable = |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 578 | (mShouldUseQuotedPrintable && |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 579 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); |
| 580 | final String encodedPhoneticFamilyName; |
| 581 | if (reallyUseQuotedPrintable) { |
| 582 | encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); |
| 583 | } else { |
| 584 | encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); |
| 585 | } |
| 586 | mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME); |
| 587 | if (shouldAppendCharsetParam(phoneticFamilyName)) { |
| 588 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 589 | mBuilder.append(mVCardCharsetParameter); |
| 590 | } |
| 591 | if (reallyUseQuotedPrintable) { |
| 592 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 593 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 594 | } |
| 595 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 596 | mBuilder.append(encodedPhoneticFamilyName); |
| 597 | mBuilder.append(VCARD_END_OF_LINE); |
| 598 | } |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) { |
| 603 | final boolean useAndroidProperty; |
| 604 | if (mIsV30) { |
| 605 | useAndroidProperty = false; |
| 606 | } else if (mUsesAndroidProperty) { |
| 607 | useAndroidProperty = true; |
| 608 | } else { |
| 609 | // There's no way to add this field. |
| 610 | return this; |
| 611 | } |
| 612 | if (contentValuesList != null) { |
| 613 | for (ContentValues contentValues : contentValuesList) { |
| 614 | final String nickname = contentValues.getAsString(Nickname.NAME); |
| 615 | if (TextUtils.isEmpty(nickname)) { |
| 616 | continue; |
| 617 | } |
| 618 | if (useAndroidProperty) { |
| 619 | appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues); |
| 620 | } else { |
| 621 | appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname); |
| 622 | } |
| 623 | } |
| 624 | } |
| 625 | return this; |
| 626 | } |
| 627 | |
| 628 | public VCardBuilder appendPhones(final List<ContentValues> contentValuesList) { |
| 629 | boolean phoneLineExists = false; |
| 630 | if (contentValuesList != null) { |
| 631 | Set<String> phoneSet = new HashSet<String>(); |
| 632 | for (ContentValues contentValues : contentValuesList) { |
| 633 | final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE); |
| 634 | final String label = contentValues.getAsString(Phone.LABEL); |
| 635 | final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY); |
| 636 | final boolean isPrimary = (isPrimaryAsInteger != null ? |
| 637 | (isPrimaryAsInteger > 0) : false); |
| 638 | String phoneNumber = contentValues.getAsString(Phone.NUMBER); |
| 639 | if (phoneNumber != null) { |
| 640 | phoneNumber = phoneNumber.trim(); |
| 641 | } |
| 642 | if (TextUtils.isEmpty(phoneNumber)) { |
| 643 | continue; |
| 644 | } |
Daisuke Miyakawa | ba2593a | 2010-04-07 17:07:57 +0900 | [diff] [blame^] | 645 | |
| 646 | // PAGER number needs unformatted "phone number". |
| 647 | final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE); |
| 648 | if (type == Phone.TYPE_PAGER || |
| 649 | VCardConfig.refrainPhoneNumberFormatting(mVCardType)) { |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 650 | phoneLineExists = true; |
| 651 | if (!phoneSet.contains(phoneNumber)) { |
| 652 | phoneSet.add(phoneNumber); |
| 653 | appendTelLine(type, label, phoneNumber, isPrimary); |
| 654 | } |
| 655 | } else { |
Daisuke Miyakawa | ba2593a | 2010-04-07 17:07:57 +0900 | [diff] [blame^] | 656 | final List<String> phoneNumberList = splitAndTrimPhoneNumbers(phoneNumber); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 657 | if (phoneNumberList.isEmpty()) { |
| 658 | continue; |
| 659 | } |
| 660 | phoneLineExists = true; |
| 661 | for (String actualPhoneNumber : phoneNumberList) { |
| 662 | if (!phoneSet.contains(actualPhoneNumber)) { |
| 663 | final int format = VCardUtils.getPhoneNumberFormat(mVCardType); |
| 664 | final String formattedPhoneNumber = |
| 665 | PhoneNumberUtils.formatNumber(actualPhoneNumber, format); |
| 666 | phoneSet.add(actualPhoneNumber); |
| 667 | appendTelLine(type, label, formattedPhoneNumber, isPrimary); |
| 668 | } |
Daisuke Miyakawa | ba2593a | 2010-04-07 17:07:57 +0900 | [diff] [blame^] | 669 | } // for (String actualPhoneNumber : phoneNumberList) { |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 670 | } |
| 671 | } |
| 672 | } |
| 673 | |
| 674 | if (!phoneLineExists && mIsDoCoMo) { |
| 675 | appendTelLine(Phone.TYPE_HOME, "", "", false); |
| 676 | } |
| 677 | |
| 678 | return this; |
| 679 | } |
| 680 | |
Daisuke Miyakawa | ba2593a | 2010-04-07 17:07:57 +0900 | [diff] [blame^] | 681 | /** |
| 682 | * <p> |
| 683 | * Splits a given string expressing phone numbers into several strings, and remove |
| 684 | * unnecessary characters inside them. The size of a returned list becomes 1 when |
| 685 | * no split is needed. |
| 686 | * </p> |
| 687 | * <p> |
| 688 | * The given number "may" have several phone numbers when the contact entry is corrupted |
| 689 | * because of its original source. |
| 690 | * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)" |
| 691 | * </p> |
| 692 | * <p> |
| 693 | * This kind of "phone numbers" will not be created with Android vCard implementation, |
| 694 | * but we may encounter them if the source of the input data has already corrupted |
| 695 | * implementation. |
| 696 | * </p> |
| 697 | * <p> |
| 698 | * To handle this case, this method first splits its input into multiple parts |
| 699 | * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and |
| 700 | * removes unnecessary strings like "(Miami)". |
| 701 | * </p> |
| 702 | * <p> |
| 703 | * Do not call this method when trimming is inappropriate for its receivers. |
| 704 | * </p> |
| 705 | */ |
| 706 | private List<String> splitAndTrimPhoneNumbers(final String phoneNumber) { |
| 707 | final List<String> phoneList = new ArrayList<String>(); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 708 | |
| 709 | StringBuilder builder = new StringBuilder(); |
| 710 | final int length = phoneNumber.length(); |
| 711 | for (int i = 0; i < length; i++) { |
| 712 | final char ch = phoneNumber.charAt(i); |
Daisuke Miyakawa | 900731de | 2010-02-03 12:42:26 -0800 | [diff] [blame] | 713 | if (Character.isDigit(ch) || ch == '+') { |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 714 | builder.append(ch); |
| 715 | } else if ((ch == ';' || ch == '\n') && builder.length() > 0) { |
| 716 | phoneList.add(builder.toString()); |
| 717 | builder = new StringBuilder(); |
| 718 | } |
| 719 | } |
| 720 | if (builder.length() > 0) { |
| 721 | phoneList.add(builder.toString()); |
| 722 | } |
| 723 | |
| 724 | return phoneList; |
| 725 | } |
| 726 | |
| 727 | public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) { |
| 728 | boolean emailAddressExists = false; |
| 729 | if (contentValuesList != null) { |
| 730 | final Set<String> addressSet = new HashSet<String>(); |
| 731 | for (ContentValues contentValues : contentValuesList) { |
| 732 | String emailAddress = contentValues.getAsString(Email.DATA); |
| 733 | if (emailAddress != null) { |
| 734 | emailAddress = emailAddress.trim(); |
| 735 | } |
| 736 | if (TextUtils.isEmpty(emailAddress)) { |
| 737 | continue; |
| 738 | } |
| 739 | Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); |
| 740 | final int type = (typeAsObject != null ? |
| 741 | typeAsObject : DEFAULT_EMAIL_TYPE); |
| 742 | final String label = contentValues.getAsString(Email.LABEL); |
| 743 | Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY); |
| 744 | final boolean isPrimary = (isPrimaryAsInteger != null ? |
| 745 | (isPrimaryAsInteger > 0) : false); |
| 746 | emailAddressExists = true; |
| 747 | if (!addressSet.contains(emailAddress)) { |
| 748 | addressSet.add(emailAddress); |
| 749 | appendEmailLine(type, label, emailAddress, isPrimary); |
| 750 | } |
| 751 | } |
| 752 | } |
| 753 | |
| 754 | if (!emailAddressExists && mIsDoCoMo) { |
| 755 | appendEmailLine(Email.TYPE_HOME, "", "", false); |
| 756 | } |
| 757 | |
| 758 | return this; |
| 759 | } |
| 760 | |
| 761 | public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) { |
| 762 | if (contentValuesList == null || contentValuesList.isEmpty()) { |
| 763 | if (mIsDoCoMo) { |
| 764 | mBuilder.append(VCardConstants.PROPERTY_ADR); |
| 765 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 766 | mBuilder.append(VCardConstants.PARAM_TYPE_HOME); |
| 767 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 768 | mBuilder.append(VCARD_END_OF_LINE); |
| 769 | } |
| 770 | } else { |
| 771 | if (mIsDoCoMo) { |
| 772 | appendPostalsForDoCoMo(contentValuesList); |
| 773 | } else { |
| 774 | appendPostalsForGeneric(contentValuesList); |
| 775 | } |
| 776 | } |
| 777 | |
| 778 | return this; |
| 779 | } |
| 780 | |
| 781 | private static final Map<Integer, Integer> sPostalTypePriorityMap; |
| 782 | |
| 783 | static { |
| 784 | sPostalTypePriorityMap = new HashMap<Integer, Integer>(); |
| 785 | sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0); |
| 786 | sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1); |
| 787 | sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2); |
| 788 | sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3); |
| 789 | } |
| 790 | |
| 791 | /** |
| 792 | * Tries to append just one line. If there's no appropriate address |
| 793 | * information, append an empty line. |
| 794 | */ |
| 795 | private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) { |
| 796 | int currentPriority = Integer.MAX_VALUE; |
| 797 | int currentType = Integer.MAX_VALUE; |
| 798 | ContentValues currentContentValues = null; |
| 799 | for (final ContentValues contentValues : contentValuesList) { |
| 800 | if (contentValues == null) { |
| 801 | continue; |
| 802 | } |
| 803 | final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); |
| 804 | final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger); |
| 805 | final int priority = |
| 806 | (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE); |
| 807 | if (priority < currentPriority) { |
| 808 | currentPriority = priority; |
| 809 | currentType = typeAsInteger; |
| 810 | currentContentValues = contentValues; |
| 811 | if (priority == 0) { |
| 812 | break; |
| 813 | } |
| 814 | } |
| 815 | } |
| 816 | |
| 817 | if (currentContentValues == null) { |
| 818 | Log.w(LOG_TAG, "Should not come here. Must have at least one postal data."); |
| 819 | return; |
| 820 | } |
| 821 | |
| 822 | final String label = currentContentValues.getAsString(StructuredPostal.LABEL); |
| 823 | appendPostalLine(currentType, label, currentContentValues, false, true); |
| 824 | } |
| 825 | |
| 826 | private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) { |
| 827 | for (final ContentValues contentValues : contentValuesList) { |
| 828 | if (contentValues == null) { |
| 829 | continue; |
| 830 | } |
| 831 | final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); |
| 832 | final int type = (typeAsInteger != null ? |
| 833 | typeAsInteger : DEFAULT_POSTAL_TYPE); |
| 834 | final String label = contentValues.getAsString(StructuredPostal.LABEL); |
| 835 | final Integer isPrimaryAsInteger = |
| 836 | contentValues.getAsInteger(StructuredPostal.IS_PRIMARY); |
| 837 | final boolean isPrimary = (isPrimaryAsInteger != null ? |
| 838 | (isPrimaryAsInteger > 0) : false); |
| 839 | appendPostalLine(type, label, contentValues, isPrimary, false); |
| 840 | } |
| 841 | } |
| 842 | |
| 843 | private static class PostalStruct { |
| 844 | final boolean reallyUseQuotedPrintable; |
| 845 | final boolean appendCharset; |
| 846 | final String addressData; |
| 847 | public PostalStruct(final boolean reallyUseQuotedPrintable, |
| 848 | final boolean appendCharset, final String addressData) { |
| 849 | this.reallyUseQuotedPrintable = reallyUseQuotedPrintable; |
| 850 | this.appendCharset = appendCharset; |
| 851 | this.addressData = addressData; |
| 852 | } |
| 853 | } |
| 854 | |
| 855 | /** |
| 856 | * @return null when there's no information available to construct the data. |
| 857 | */ |
| 858 | private PostalStruct tryConstructPostalStruct(ContentValues contentValues) { |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 859 | // adr-value = 0*6(text-value ";") text-value |
| 860 | // ; PO Box, Extended Address, Street, Locality, Region, Postal |
| 861 | // ; Code, Country Name |
| 862 | final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX); |
Daisuke Miyakawa | 639b0f0 | 2009-11-20 07:06:40 +0900 | [diff] [blame] | 863 | final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD); |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 864 | final String rawStreet = contentValues.getAsString(StructuredPostal.STREET); |
| 865 | final String rawLocality = contentValues.getAsString(StructuredPostal.CITY); |
| 866 | final String rawRegion = contentValues.getAsString(StructuredPostal.REGION); |
| 867 | final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE); |
| 868 | final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY); |
| 869 | final String[] rawAddressArray = new String[]{ |
Daisuke Miyakawa | 639b0f0 | 2009-11-20 07:06:40 +0900 | [diff] [blame] | 870 | rawPoBox, rawNeighborhood, rawStreet, rawLocality, |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 871 | rawRegion, rawPostalCode, rawCountry}; |
| 872 | if (!VCardUtils.areAllEmpty(rawAddressArray)) { |
| 873 | final boolean reallyUseQuotedPrintable = |
| 874 | (mShouldUseQuotedPrintable && |
| 875 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray)); |
| 876 | final boolean appendCharset = |
| 877 | !VCardUtils.containsOnlyPrintableAscii(rawAddressArray); |
| 878 | final String encodedPoBox; |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 879 | final String encodedStreet; |
| 880 | final String encodedLocality; |
| 881 | final String encodedRegion; |
| 882 | final String encodedPostalCode; |
| 883 | final String encodedCountry; |
Daisuke Miyakawa | 639b0f0 | 2009-11-20 07:06:40 +0900 | [diff] [blame] | 884 | final String encodedNeighborhood; |
| 885 | |
| 886 | final String rawLocality2; |
| 887 | // This looks inefficient since we encode rawLocality and rawNeighborhood twice, |
| 888 | // but this is intentional. |
| 889 | // |
| 890 | // QP encoding may add line feeds when needed and the result of |
| 891 | // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood) |
| 892 | // may be different from |
| 893 | // - encodedLocality + " " + encodedNeighborhood. |
| 894 | // |
| 895 | // We use safer way. |
| 896 | if (TextUtils.isEmpty(rawLocality)) { |
| 897 | if (TextUtils.isEmpty(rawNeighborhood)) { |
| 898 | rawLocality2 = ""; |
| 899 | } else { |
| 900 | rawLocality2 = rawNeighborhood; |
| 901 | } |
| 902 | } else { |
| 903 | if (TextUtils.isEmpty(rawNeighborhood)) { |
| 904 | rawLocality2 = rawLocality; |
| 905 | } else { |
| 906 | rawLocality2 = rawLocality + " " + rawNeighborhood; |
| 907 | } |
| 908 | } |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 909 | if (reallyUseQuotedPrintable) { |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 910 | encodedPoBox = encodeQuotedPrintable(rawPoBox); |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 911 | encodedStreet = encodeQuotedPrintable(rawStreet); |
Daisuke Miyakawa | 639b0f0 | 2009-11-20 07:06:40 +0900 | [diff] [blame] | 912 | encodedLocality = encodeQuotedPrintable(rawLocality2); |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 913 | encodedRegion = encodeQuotedPrintable(rawRegion); |
| 914 | encodedPostalCode = encodeQuotedPrintable(rawPostalCode); |
| 915 | encodedCountry = encodeQuotedPrintable(rawCountry); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 916 | } else { |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 917 | encodedPoBox = escapeCharacters(rawPoBox); |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 918 | encodedStreet = escapeCharacters(rawStreet); |
Daisuke Miyakawa | 639b0f0 | 2009-11-20 07:06:40 +0900 | [diff] [blame] | 919 | encodedLocality = escapeCharacters(rawLocality2); |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 920 | encodedRegion = escapeCharacters(rawRegion); |
| 921 | encodedPostalCode = escapeCharacters(rawPostalCode); |
| 922 | encodedCountry = escapeCharacters(rawCountry); |
Daisuke Miyakawa | 639b0f0 | 2009-11-20 07:06:40 +0900 | [diff] [blame] | 923 | encodedNeighborhood = escapeCharacters(rawNeighborhood); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 924 | } |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 925 | final StringBuffer addressBuffer = new StringBuffer(); |
| 926 | addressBuffer.append(encodedPoBox); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 927 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 928 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 929 | addressBuffer.append(encodedStreet); |
| 930 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 931 | addressBuffer.append(encodedLocality); |
| 932 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 933 | addressBuffer.append(encodedRegion); |
| 934 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 935 | addressBuffer.append(encodedPostalCode); |
| 936 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 937 | addressBuffer.append(encodedCountry); |
| 938 | return new PostalStruct( |
| 939 | reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); |
| 940 | } else { // VCardUtils.areAllEmpty(rawAddressArray) == true |
| 941 | // Try to use FORMATTED_ADDRESS instead. |
| 942 | final String rawFormattedAddress = |
| 943 | contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS); |
| 944 | if (TextUtils.isEmpty(rawFormattedAddress)) { |
| 945 | return null; |
| 946 | } |
| 947 | final boolean reallyUseQuotedPrintable = |
| 948 | (mShouldUseQuotedPrintable && |
| 949 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress)); |
| 950 | final boolean appendCharset = |
| 951 | !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress); |
| 952 | final String encodedFormattedAddress; |
| 953 | if (reallyUseQuotedPrintable) { |
| 954 | encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress); |
| 955 | } else { |
| 956 | encodedFormattedAddress = escapeCharacters(rawFormattedAddress); |
| 957 | } |
| 958 | |
| 959 | // We use the second value ("Extended Address") just because Japanese mobile phones |
| 960 | // do so. If the other importer expects the value be in the other field, some flag may |
| 961 | // be needed. |
| 962 | final StringBuffer addressBuffer = new StringBuffer(); |
| 963 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 964 | addressBuffer.append(encodedFormattedAddress); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 965 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 966 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 967 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 968 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 969 | addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| 970 | return new PostalStruct( |
| 971 | reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); |
| 972 | } |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 973 | } |
| 974 | |
| 975 | public VCardBuilder appendIms(final List<ContentValues> contentValuesList) { |
| 976 | if (contentValuesList != null) { |
| 977 | for (ContentValues contentValues : contentValuesList) { |
| 978 | final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL); |
| 979 | if (protocolAsObject == null) { |
| 980 | continue; |
| 981 | } |
| 982 | final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject); |
| 983 | if (propertyName == null) { |
| 984 | continue; |
| 985 | } |
| 986 | String data = contentValues.getAsString(Im.DATA); |
| 987 | if (data != null) { |
| 988 | data = data.trim(); |
| 989 | } |
| 990 | if (TextUtils.isEmpty(data)) { |
| 991 | continue; |
| 992 | } |
| 993 | final String typeAsString; |
| 994 | { |
| 995 | final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE); |
| 996 | switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) { |
| 997 | case Im.TYPE_HOME: { |
| 998 | typeAsString = VCardConstants.PARAM_TYPE_HOME; |
| 999 | break; |
| 1000 | } |
| 1001 | case Im.TYPE_WORK: { |
| 1002 | typeAsString = VCardConstants.PARAM_TYPE_WORK; |
| 1003 | break; |
| 1004 | } |
| 1005 | case Im.TYPE_CUSTOM: { |
| 1006 | final String label = contentValues.getAsString(Im.LABEL); |
| 1007 | typeAsString = (label != null ? "X-" + label : null); |
| 1008 | break; |
| 1009 | } |
| 1010 | case Im.TYPE_OTHER: // Ignore |
| 1011 | default: { |
| 1012 | typeAsString = null; |
| 1013 | break; |
| 1014 | } |
| 1015 | } |
| 1016 | } |
| 1017 | |
| 1018 | final List<String> parameterList = new ArrayList<String>(); |
| 1019 | if (!TextUtils.isEmpty(typeAsString)) { |
| 1020 | parameterList.add(typeAsString); |
| 1021 | } |
| 1022 | final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY); |
| 1023 | final boolean isPrimary = (isPrimaryAsInteger != null ? |
| 1024 | (isPrimaryAsInteger > 0) : false); |
| 1025 | if (isPrimary) { |
| 1026 | parameterList.add(VCardConstants.PARAM_TYPE_PREF); |
| 1027 | } |
| 1028 | |
| 1029 | appendLineWithCharsetAndQPDetection(propertyName, parameterList, data); |
| 1030 | } |
| 1031 | } |
| 1032 | return this; |
| 1033 | } |
| 1034 | |
| 1035 | public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) { |
| 1036 | if (contentValuesList != null) { |
| 1037 | for (ContentValues contentValues : contentValuesList) { |
| 1038 | String website = contentValues.getAsString(Website.URL); |
| 1039 | if (website != null) { |
| 1040 | website = website.trim(); |
| 1041 | } |
| 1042 | |
| 1043 | // Note: vCard 3.0 does not allow any parameter addition toward "URL" |
| 1044 | // property, while there's no document in vCard 2.1. |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1045 | if (!TextUtils.isEmpty(website)) { |
Daisuke Miyakawa | 49c0dec | 2009-11-18 17:11:43 +0900 | [diff] [blame] | 1046 | appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1047 | } |
| 1048 | } |
| 1049 | } |
| 1050 | return this; |
| 1051 | } |
| 1052 | |
| 1053 | public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) { |
| 1054 | if (contentValuesList != null) { |
| 1055 | for (ContentValues contentValues : contentValuesList) { |
| 1056 | String company = contentValues.getAsString(Organization.COMPANY); |
| 1057 | if (company != null) { |
| 1058 | company = company.trim(); |
| 1059 | } |
| 1060 | String department = contentValues.getAsString(Organization.DEPARTMENT); |
| 1061 | if (department != null) { |
| 1062 | department = department.trim(); |
| 1063 | } |
| 1064 | String title = contentValues.getAsString(Organization.TITLE); |
| 1065 | if (title != null) { |
| 1066 | title = title.trim(); |
| 1067 | } |
| 1068 | |
| 1069 | StringBuilder orgBuilder = new StringBuilder(); |
| 1070 | if (!TextUtils.isEmpty(company)) { |
| 1071 | orgBuilder.append(company); |
| 1072 | } |
| 1073 | if (!TextUtils.isEmpty(department)) { |
| 1074 | if (orgBuilder.length() > 0) { |
| 1075 | orgBuilder.append(';'); |
| 1076 | } |
| 1077 | orgBuilder.append(department); |
| 1078 | } |
| 1079 | final String orgline = orgBuilder.toString(); |
| 1080 | appendLine(VCardConstants.PROPERTY_ORG, orgline, |
| 1081 | !VCardUtils.containsOnlyPrintableAscii(orgline), |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 1082 | (mShouldUseQuotedPrintable && |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1083 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline))); |
| 1084 | |
| 1085 | if (!TextUtils.isEmpty(title)) { |
| 1086 | appendLine(VCardConstants.PROPERTY_TITLE, title, |
| 1087 | !VCardUtils.containsOnlyPrintableAscii(title), |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 1088 | (mShouldUseQuotedPrintable && |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1089 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); |
| 1090 | } |
| 1091 | } |
| 1092 | } |
| 1093 | return this; |
| 1094 | } |
| 1095 | |
| 1096 | public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) { |
| 1097 | if (contentValuesList != null) { |
| 1098 | for (ContentValues contentValues : contentValuesList) { |
| 1099 | if (contentValues == null) { |
| 1100 | continue; |
| 1101 | } |
| 1102 | byte[] data = contentValues.getAsByteArray(Photo.PHOTO); |
| 1103 | if (data == null) { |
| 1104 | continue; |
| 1105 | } |
Daisuke Miyakawa | 49c0dec | 2009-11-18 17:11:43 +0900 | [diff] [blame] | 1106 | final String photoType = VCardUtils.guessImageType(data); |
| 1107 | if (photoType == null) { |
| 1108 | Log.d(LOG_TAG, "Unknown photo type. Ignored."); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1109 | continue; |
| 1110 | } |
| 1111 | final String photoString = new String(Base64.encodeBase64(data)); |
| 1112 | if (!TextUtils.isEmpty(photoString)) { |
| 1113 | appendPhotoLine(photoString, photoType); |
| 1114 | } |
| 1115 | } |
| 1116 | } |
| 1117 | return this; |
| 1118 | } |
| 1119 | |
| 1120 | public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) { |
| 1121 | if (contentValuesList != null) { |
| 1122 | if (mOnlyOneNoteFieldIsAvailable) { |
Daisuke Miyakawa | 839c036 | 2009-11-18 10:25:09 +0900 | [diff] [blame] | 1123 | final StringBuilder noteBuilder = new StringBuilder(); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1124 | boolean first = true; |
| 1125 | for (final ContentValues contentValues : contentValuesList) { |
| 1126 | String note = contentValues.getAsString(Note.NOTE); |
| 1127 | if (note == null) { |
| 1128 | note = ""; |
| 1129 | } |
| 1130 | if (note.length() > 0) { |
| 1131 | if (first) { |
| 1132 | first = false; |
| 1133 | } else { |
| 1134 | noteBuilder.append('\n'); |
| 1135 | } |
| 1136 | noteBuilder.append(note); |
| 1137 | } |
| 1138 | } |
| 1139 | final String noteStr = noteBuilder.toString(); |
| 1140 | // This means we scan noteStr completely twice, which is redundant. |
| 1141 | // But for now, we assume this is not so time-consuming.. |
| 1142 | final boolean shouldAppendCharsetInfo = |
| 1143 | !VCardUtils.containsOnlyPrintableAscii(noteStr); |
| 1144 | final boolean reallyUseQuotedPrintable = |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 1145 | (mShouldUseQuotedPrintable && |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1146 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); |
| 1147 | appendLine(VCardConstants.PROPERTY_NOTE, noteStr, |
| 1148 | shouldAppendCharsetInfo, reallyUseQuotedPrintable); |
| 1149 | } else { |
| 1150 | for (ContentValues contentValues : contentValuesList) { |
| 1151 | final String noteStr = contentValues.getAsString(Note.NOTE); |
| 1152 | if (!TextUtils.isEmpty(noteStr)) { |
| 1153 | final boolean shouldAppendCharsetInfo = |
| 1154 | !VCardUtils.containsOnlyPrintableAscii(noteStr); |
| 1155 | final boolean reallyUseQuotedPrintable = |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 1156 | (mShouldUseQuotedPrintable && |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1157 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); |
| 1158 | appendLine(VCardConstants.PROPERTY_NOTE, noteStr, |
| 1159 | shouldAppendCharsetInfo, reallyUseQuotedPrintable); |
| 1160 | } |
| 1161 | } |
| 1162 | } |
| 1163 | } |
| 1164 | return this; |
| 1165 | } |
| 1166 | |
| 1167 | public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) { |
| 1168 | if (contentValuesList != null) { |
| 1169 | String primaryBirthday = null; |
| 1170 | String secondaryBirthday = null; |
| 1171 | for (final ContentValues contentValues : contentValuesList) { |
| 1172 | if (contentValues == null) { |
| 1173 | continue; |
| 1174 | } |
| 1175 | final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE); |
| 1176 | final int eventType; |
| 1177 | if (eventTypeAsInteger != null) { |
| 1178 | eventType = eventTypeAsInteger; |
| 1179 | } else { |
| 1180 | eventType = Event.TYPE_OTHER; |
| 1181 | } |
| 1182 | if (eventType == Event.TYPE_BIRTHDAY) { |
| 1183 | final String birthdayCandidate = contentValues.getAsString(Event.START_DATE); |
| 1184 | if (birthdayCandidate == null) { |
| 1185 | continue; |
| 1186 | } |
| 1187 | final Integer isSuperPrimaryAsInteger = |
| 1188 | contentValues.getAsInteger(Event.IS_SUPER_PRIMARY); |
| 1189 | final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ? |
| 1190 | (isSuperPrimaryAsInteger > 0) : false); |
| 1191 | if (isSuperPrimary) { |
| 1192 | // "super primary" birthday should the prefered one. |
| 1193 | primaryBirthday = birthdayCandidate; |
| 1194 | break; |
| 1195 | } |
| 1196 | final Integer isPrimaryAsInteger = |
| 1197 | contentValues.getAsInteger(Event.IS_PRIMARY); |
| 1198 | final boolean isPrimary = (isPrimaryAsInteger != null ? |
| 1199 | (isPrimaryAsInteger > 0) : false); |
| 1200 | if (isPrimary) { |
| 1201 | // We don't break here since "super primary" birthday may exist later. |
| 1202 | primaryBirthday = birthdayCandidate; |
| 1203 | } else if (secondaryBirthday == null) { |
| 1204 | // First entry is set to the "secondary" candidate. |
| 1205 | secondaryBirthday = birthdayCandidate; |
| 1206 | } |
| 1207 | } else if (mUsesAndroidProperty) { |
| 1208 | // Event types other than Birthday is not supported by vCard. |
| 1209 | appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues); |
| 1210 | } |
| 1211 | } |
| 1212 | if (primaryBirthday != null) { |
| 1213 | appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, |
| 1214 | primaryBirthday.trim()); |
| 1215 | } else if (secondaryBirthday != null){ |
| 1216 | appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, |
| 1217 | secondaryBirthday.trim()); |
| 1218 | } |
| 1219 | } |
| 1220 | return this; |
| 1221 | } |
| 1222 | |
| 1223 | public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) { |
| 1224 | if (mUsesAndroidProperty && contentValuesList != null) { |
| 1225 | for (final ContentValues contentValues : contentValuesList) { |
| 1226 | if (contentValues == null) { |
| 1227 | continue; |
| 1228 | } |
| 1229 | appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues); |
| 1230 | } |
| 1231 | } |
| 1232 | return this; |
| 1233 | } |
| 1234 | |
| 1235 | public void appendPostalLine(final int type, final String label, |
| 1236 | final ContentValues contentValues, |
| 1237 | final boolean isPrimary, final boolean emitLineEveryTime) { |
| 1238 | final boolean reallyUseQuotedPrintable; |
| 1239 | final boolean appendCharset; |
| 1240 | final String addressValue; |
| 1241 | { |
| 1242 | PostalStruct postalStruct = tryConstructPostalStruct(contentValues); |
| 1243 | if (postalStruct == null) { |
| 1244 | if (emitLineEveryTime) { |
| 1245 | reallyUseQuotedPrintable = false; |
| 1246 | appendCharset = false; |
| 1247 | addressValue = ""; |
| 1248 | } else { |
| 1249 | return; |
| 1250 | } |
| 1251 | } else { |
| 1252 | reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable; |
| 1253 | appendCharset = postalStruct.appendCharset; |
| 1254 | addressValue = postalStruct.addressData; |
| 1255 | } |
| 1256 | } |
| 1257 | |
| 1258 | List<String> parameterList = new ArrayList<String>(); |
| 1259 | if (isPrimary) { |
| 1260 | parameterList.add(VCardConstants.PARAM_TYPE_PREF); |
| 1261 | } |
| 1262 | switch (type) { |
| 1263 | case StructuredPostal.TYPE_HOME: { |
| 1264 | parameterList.add(VCardConstants.PARAM_TYPE_HOME); |
| 1265 | break; |
| 1266 | } |
| 1267 | case StructuredPostal.TYPE_WORK: { |
| 1268 | parameterList.add(VCardConstants.PARAM_TYPE_WORK); |
| 1269 | break; |
| 1270 | } |
| 1271 | case StructuredPostal.TYPE_CUSTOM: { |
| 1272 | if (!TextUtils.isEmpty(label) |
| 1273 | && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { |
| 1274 | // We're not sure whether the label is valid in the spec |
| 1275 | // ("IANA-token" in the vCard 3.0 is unclear...) |
| 1276 | // Just for safety, we add "X-" at the beggining of each label. |
| 1277 | // Also checks the label obeys with vCard 3.0 spec. |
| 1278 | parameterList.add("X-" + label); |
| 1279 | } |
| 1280 | break; |
| 1281 | } |
| 1282 | case StructuredPostal.TYPE_OTHER: { |
| 1283 | break; |
| 1284 | } |
| 1285 | default: { |
| 1286 | Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); |
| 1287 | break; |
| 1288 | } |
| 1289 | } |
| 1290 | |
| 1291 | mBuilder.append(VCardConstants.PROPERTY_ADR); |
| 1292 | if (!parameterList.isEmpty()) { |
| 1293 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1294 | appendTypeParameters(parameterList); |
| 1295 | } |
| 1296 | if (appendCharset) { |
| 1297 | // Strictly, vCard 3.0 does not allow exporters to emit charset information, |
| 1298 | // but we will add it since the information should be useful for importers, |
| 1299 | // |
| 1300 | // Assume no parser does not emit error with this parameter in vCard 3.0. |
| 1301 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1302 | mBuilder.append(mVCardCharsetParameter); |
| 1303 | } |
| 1304 | if (reallyUseQuotedPrintable) { |
| 1305 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1306 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 1307 | } |
| 1308 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 1309 | mBuilder.append(addressValue); |
| 1310 | mBuilder.append(VCARD_END_OF_LINE); |
| 1311 | } |
| 1312 | |
| 1313 | public void appendEmailLine(final int type, final String label, |
| 1314 | final String rawValue, final boolean isPrimary) { |
| 1315 | final String typeAsString; |
| 1316 | switch (type) { |
| 1317 | case Email.TYPE_CUSTOM: { |
| 1318 | if (VCardUtils.isMobilePhoneLabel(label)) { |
| 1319 | typeAsString = VCardConstants.PARAM_TYPE_CELL; |
| 1320 | } else if (!TextUtils.isEmpty(label) |
| 1321 | && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { |
| 1322 | typeAsString = "X-" + label; |
| 1323 | } else { |
| 1324 | typeAsString = null; |
| 1325 | } |
| 1326 | break; |
| 1327 | } |
| 1328 | case Email.TYPE_HOME: { |
| 1329 | typeAsString = VCardConstants.PARAM_TYPE_HOME; |
| 1330 | break; |
| 1331 | } |
| 1332 | case Email.TYPE_WORK: { |
| 1333 | typeAsString = VCardConstants.PARAM_TYPE_WORK; |
| 1334 | break; |
| 1335 | } |
| 1336 | case Email.TYPE_OTHER: { |
| 1337 | typeAsString = null; |
| 1338 | break; |
| 1339 | } |
| 1340 | case Email.TYPE_MOBILE: { |
| 1341 | typeAsString = VCardConstants.PARAM_TYPE_CELL; |
| 1342 | break; |
| 1343 | } |
| 1344 | default: { |
| 1345 | Log.e(LOG_TAG, "Unknown Email type: " + type); |
| 1346 | typeAsString = null; |
| 1347 | break; |
| 1348 | } |
| 1349 | } |
| 1350 | |
| 1351 | final List<String> parameterList = new ArrayList<String>(); |
| 1352 | if (isPrimary) { |
| 1353 | parameterList.add(VCardConstants.PARAM_TYPE_PREF); |
| 1354 | } |
| 1355 | if (!TextUtils.isEmpty(typeAsString)) { |
| 1356 | parameterList.add(typeAsString); |
| 1357 | } |
| 1358 | |
| 1359 | appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList, |
| 1360 | rawValue); |
| 1361 | } |
| 1362 | |
| 1363 | public void appendTelLine(final Integer typeAsInteger, final String label, |
| 1364 | final String encodedValue, boolean isPrimary) { |
| 1365 | mBuilder.append(VCardConstants.PROPERTY_TEL); |
| 1366 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1367 | |
| 1368 | final int type; |
| 1369 | if (typeAsInteger == null) { |
| 1370 | type = Phone.TYPE_OTHER; |
| 1371 | } else { |
| 1372 | type = typeAsInteger; |
| 1373 | } |
| 1374 | |
| 1375 | ArrayList<String> parameterList = new ArrayList<String>(); |
| 1376 | switch (type) { |
| 1377 | case Phone.TYPE_HOME: { |
| 1378 | parameterList.addAll( |
| 1379 | Arrays.asList(VCardConstants.PARAM_TYPE_HOME)); |
| 1380 | break; |
| 1381 | } |
| 1382 | case Phone.TYPE_WORK: { |
| 1383 | parameterList.addAll( |
| 1384 | Arrays.asList(VCardConstants.PARAM_TYPE_WORK)); |
| 1385 | break; |
| 1386 | } |
| 1387 | case Phone.TYPE_FAX_HOME: { |
| 1388 | parameterList.addAll( |
| 1389 | Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX)); |
| 1390 | break; |
| 1391 | } |
| 1392 | case Phone.TYPE_FAX_WORK: { |
| 1393 | parameterList.addAll( |
| 1394 | Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX)); |
| 1395 | break; |
| 1396 | } |
| 1397 | case Phone.TYPE_MOBILE: { |
| 1398 | parameterList.add(VCardConstants.PARAM_TYPE_CELL); |
| 1399 | break; |
| 1400 | } |
| 1401 | case Phone.TYPE_PAGER: { |
| 1402 | if (mIsDoCoMo) { |
| 1403 | // Not sure about the reason, but previous implementation had |
| 1404 | // used "VOICE" instead of "PAGER" |
| 1405 | parameterList.add(VCardConstants.PARAM_TYPE_VOICE); |
| 1406 | } else { |
| 1407 | parameterList.add(VCardConstants.PARAM_TYPE_PAGER); |
| 1408 | } |
| 1409 | break; |
| 1410 | } |
| 1411 | case Phone.TYPE_OTHER: { |
| 1412 | parameterList.add(VCardConstants.PARAM_TYPE_VOICE); |
| 1413 | break; |
| 1414 | } |
| 1415 | case Phone.TYPE_CAR: { |
| 1416 | parameterList.add(VCardConstants.PARAM_TYPE_CAR); |
| 1417 | break; |
| 1418 | } |
| 1419 | case Phone.TYPE_COMPANY_MAIN: { |
| 1420 | // There's no relevant field in vCard (at least 2.1). |
| 1421 | parameterList.add(VCardConstants.PARAM_TYPE_WORK); |
| 1422 | isPrimary = true; |
| 1423 | break; |
| 1424 | } |
| 1425 | case Phone.TYPE_ISDN: { |
| 1426 | parameterList.add(VCardConstants.PARAM_TYPE_ISDN); |
| 1427 | break; |
| 1428 | } |
| 1429 | case Phone.TYPE_MAIN: { |
| 1430 | isPrimary = true; |
| 1431 | break; |
| 1432 | } |
| 1433 | case Phone.TYPE_OTHER_FAX: { |
| 1434 | parameterList.add(VCardConstants.PARAM_TYPE_FAX); |
| 1435 | break; |
| 1436 | } |
| 1437 | case Phone.TYPE_TELEX: { |
| 1438 | parameterList.add(VCardConstants.PARAM_TYPE_TLX); |
| 1439 | break; |
| 1440 | } |
| 1441 | case Phone.TYPE_WORK_MOBILE: { |
| 1442 | parameterList.addAll( |
| 1443 | Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL)); |
| 1444 | break; |
| 1445 | } |
| 1446 | case Phone.TYPE_WORK_PAGER: { |
| 1447 | parameterList.add(VCardConstants.PARAM_TYPE_WORK); |
| 1448 | // See above. |
| 1449 | if (mIsDoCoMo) { |
| 1450 | parameterList.add(VCardConstants.PARAM_TYPE_VOICE); |
| 1451 | } else { |
| 1452 | parameterList.add(VCardConstants.PARAM_TYPE_PAGER); |
| 1453 | } |
| 1454 | break; |
| 1455 | } |
| 1456 | case Phone.TYPE_MMS: { |
| 1457 | parameterList.add(VCardConstants.PARAM_TYPE_MSG); |
| 1458 | break; |
| 1459 | } |
| 1460 | case Phone.TYPE_CUSTOM: { |
| 1461 | if (TextUtils.isEmpty(label)) { |
| 1462 | // Just ignore the custom type. |
| 1463 | parameterList.add(VCardConstants.PARAM_TYPE_VOICE); |
| 1464 | } else if (VCardUtils.isMobilePhoneLabel(label)) { |
| 1465 | parameterList.add(VCardConstants.PARAM_TYPE_CELL); |
| 1466 | } else { |
| 1467 | final String upperLabel = label.toUpperCase(); |
| 1468 | if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) { |
| 1469 | parameterList.add(upperLabel); |
| 1470 | } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) { |
| 1471 | // Note: Strictly, vCard 2.1 does not allow "X-" parameter without |
| 1472 | // "TYPE=" string. |
| 1473 | parameterList.add("X-" + label); |
| 1474 | } |
| 1475 | } |
| 1476 | break; |
| 1477 | } |
| 1478 | case Phone.TYPE_RADIO: |
| 1479 | case Phone.TYPE_TTY_TDD: |
| 1480 | default: { |
| 1481 | break; |
| 1482 | } |
| 1483 | } |
| 1484 | |
| 1485 | if (isPrimary) { |
| 1486 | parameterList.add(VCardConstants.PARAM_TYPE_PREF); |
| 1487 | } |
| 1488 | |
| 1489 | if (parameterList.isEmpty()) { |
| 1490 | appendUncommonPhoneType(mBuilder, type); |
| 1491 | } else { |
| 1492 | appendTypeParameters(parameterList); |
| 1493 | } |
| 1494 | |
| 1495 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 1496 | mBuilder.append(encodedValue); |
| 1497 | mBuilder.append(VCARD_END_OF_LINE); |
| 1498 | } |
| 1499 | |
| 1500 | /** |
| 1501 | * Appends phone type string which may not be available in some devices. |
| 1502 | */ |
| 1503 | private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) { |
| 1504 | if (mIsDoCoMo) { |
| 1505 | // The previous implementation for DoCoMo had been conservative |
| 1506 | // about miscellaneous types. |
| 1507 | builder.append(VCardConstants.PARAM_TYPE_VOICE); |
| 1508 | } else { |
| 1509 | String phoneType = VCardUtils.getPhoneTypeString(type); |
| 1510 | if (phoneType != null) { |
| 1511 | appendTypeParameter(phoneType); |
| 1512 | } else { |
| 1513 | Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); |
| 1514 | } |
| 1515 | } |
| 1516 | } |
| 1517 | |
| 1518 | /** |
| 1519 | * @param encodedValue Must be encoded by BASE64 |
| 1520 | * @param photoType |
| 1521 | */ |
| 1522 | public void appendPhotoLine(final String encodedValue, final String photoType) { |
| 1523 | StringBuilder tmpBuilder = new StringBuilder(); |
| 1524 | tmpBuilder.append(VCardConstants.PROPERTY_PHOTO); |
| 1525 | tmpBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1526 | if (mIsV30) { |
| 1527 | tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30); |
| 1528 | } else { |
| 1529 | tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21); |
| 1530 | } |
| 1531 | tmpBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1532 | appendTypeParameter(tmpBuilder, photoType); |
| 1533 | tmpBuilder.append(VCARD_DATA_SEPARATOR); |
| 1534 | tmpBuilder.append(encodedValue); |
| 1535 | |
| 1536 | final String tmpStr = tmpBuilder.toString(); |
| 1537 | tmpBuilder = new StringBuilder(); |
| 1538 | int lineCount = 0; |
| 1539 | final int length = tmpStr.length(); |
| 1540 | final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30 |
| 1541 | - VCARD_END_OF_LINE.length(); |
| 1542 | final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length(); |
| 1543 | int maxNum = maxNumForFirstLine; |
| 1544 | for (int i = 0; i < length; i++) { |
| 1545 | tmpBuilder.append(tmpStr.charAt(i)); |
| 1546 | lineCount++; |
| 1547 | if (lineCount > maxNum) { |
| 1548 | tmpBuilder.append(VCARD_END_OF_LINE); |
| 1549 | tmpBuilder.append(VCARD_WS); |
| 1550 | maxNum = maxNumInGeneral; |
| 1551 | lineCount = 0; |
| 1552 | } |
| 1553 | } |
| 1554 | mBuilder.append(tmpBuilder.toString()); |
| 1555 | mBuilder.append(VCARD_END_OF_LINE); |
| 1556 | mBuilder.append(VCARD_END_OF_LINE); |
| 1557 | } |
| 1558 | |
| 1559 | public void appendAndroidSpecificProperty(final String mimeType, ContentValues contentValues) { |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1560 | if (!sAllowedAndroidPropertySet.contains(mimeType)) { |
| 1561 | return; |
| 1562 | } |
Daisuke Miyakawa | a750fdd | 2009-11-20 16:09:34 +0900 | [diff] [blame] | 1563 | final List<String> rawValueList = new ArrayList<String>(); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1564 | for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) { |
| 1565 | String value = contentValues.getAsString("data" + i); |
| 1566 | if (value == null) { |
| 1567 | value = ""; |
| 1568 | } |
| 1569 | rawValueList.add(value); |
| 1570 | } |
| 1571 | |
Daisuke Miyakawa | a750fdd | 2009-11-20 16:09:34 +0900 | [diff] [blame] | 1572 | boolean needCharset = |
| 1573 | (mShouldAppendCharsetParam && |
| 1574 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); |
| 1575 | boolean reallyUseQuotedPrintable = |
| 1576 | (mShouldUseQuotedPrintable && |
| 1577 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); |
| 1578 | mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM); |
| 1579 | if (needCharset) { |
| 1580 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1581 | mBuilder.append(mVCardCharsetParameter); |
| 1582 | } |
| 1583 | if (reallyUseQuotedPrintable) { |
| 1584 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1585 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 1586 | } |
| 1587 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 1588 | mBuilder.append(mimeType); // Should not be encoded. |
| 1589 | for (String rawValue : rawValueList) { |
| 1590 | final String encodedValue; |
| 1591 | if (reallyUseQuotedPrintable) { |
| 1592 | encodedValue = encodeQuotedPrintable(rawValue); |
| 1593 | } else { |
| 1594 | // TODO: one line may be too huge, which may be invalid in vCard 3.0 |
| 1595 | // (which says "When generating a content line, lines longer than |
| 1596 | // 75 characters SHOULD be folded"), though several |
| 1597 | // (even well-known) applications do not care this. |
| 1598 | encodedValue = escapeCharacters(rawValue); |
| 1599 | } |
| 1600 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 1601 | mBuilder.append(encodedValue); |
| 1602 | } |
| 1603 | mBuilder.append(VCARD_END_OF_LINE); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1604 | } |
| 1605 | |
| 1606 | public void appendLineWithCharsetAndQPDetection(final String propertyName, |
| 1607 | final String rawValue) { |
| 1608 | appendLineWithCharsetAndQPDetection(propertyName, null, rawValue); |
| 1609 | } |
| 1610 | |
Daisuke Miyakawa | a750fdd | 2009-11-20 16:09:34 +0900 | [diff] [blame] | 1611 | public void appendLineWithCharsetAndQPDetection( |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1612 | final String propertyName, final List<String> rawValueList) { |
| 1613 | appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList); |
| 1614 | } |
| 1615 | |
| 1616 | public void appendLineWithCharsetAndQPDetection(final String propertyName, |
| 1617 | final List<String> parameterList, final String rawValue) { |
| 1618 | final boolean needCharset = |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 1619 | !VCardUtils.containsOnlyPrintableAscii(rawValue); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1620 | final boolean reallyUseQuotedPrintable = |
Daisuke Miyakawa | d2145b9 | 2009-11-17 17:03:23 +0900 | [diff] [blame] | 1621 | (mShouldUseQuotedPrintable && |
| 1622 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue)); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1623 | appendLine(propertyName, parameterList, |
| 1624 | rawValue, needCharset, reallyUseQuotedPrintable); |
| 1625 | } |
| 1626 | |
| 1627 | public void appendLineWithCharsetAndQPDetection(final String propertyName, |
| 1628 | final List<String> parameterList, final List<String> rawValueList) { |
Daisuke Miyakawa | a750fdd | 2009-11-20 16:09:34 +0900 | [diff] [blame] | 1629 | boolean needCharset = |
| 1630 | (mShouldAppendCharsetParam && |
| 1631 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); |
| 1632 | boolean reallyUseQuotedPrintable = |
| 1633 | (mShouldUseQuotedPrintable && |
| 1634 | !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1635 | appendLine(propertyName, parameterList, rawValueList, |
| 1636 | needCharset, reallyUseQuotedPrintable); |
| 1637 | } |
| 1638 | |
| 1639 | /** |
| 1640 | * Appends one line with a given property name and value. |
| 1641 | */ |
| 1642 | public void appendLine(final String propertyName, final String rawValue) { |
| 1643 | appendLine(propertyName, rawValue, false, false); |
| 1644 | } |
| 1645 | |
| 1646 | public void appendLine(final String propertyName, final List<String> rawValueList) { |
| 1647 | appendLine(propertyName, rawValueList, false, false); |
| 1648 | } |
| 1649 | |
| 1650 | public void appendLine(final String propertyName, |
Daisuke Miyakawa | a750fdd | 2009-11-20 16:09:34 +0900 | [diff] [blame] | 1651 | final String rawValue, final boolean needCharset, |
| 1652 | boolean reallyUseQuotedPrintable) { |
| 1653 | appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1654 | } |
| 1655 | |
| 1656 | public void appendLine(final String propertyName, final List<String> parameterList, |
| 1657 | final String rawValue) { |
| 1658 | appendLine(propertyName, parameterList, rawValue, false, false); |
| 1659 | } |
| 1660 | |
| 1661 | public void appendLine(final String propertyName, final List<String> parameterList, |
Daisuke Miyakawa | a750fdd | 2009-11-20 16:09:34 +0900 | [diff] [blame] | 1662 | final String rawValue, final boolean needCharset, |
| 1663 | boolean reallyUseQuotedPrintable) { |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1664 | mBuilder.append(propertyName); |
| 1665 | if (parameterList != null && parameterList.size() > 0) { |
| 1666 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1667 | appendTypeParameters(parameterList); |
| 1668 | } |
| 1669 | if (needCharset) { |
| 1670 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1671 | mBuilder.append(mVCardCharsetParameter); |
| 1672 | } |
| 1673 | |
| 1674 | final String encodedValue; |
Daisuke Miyakawa | a750fdd | 2009-11-20 16:09:34 +0900 | [diff] [blame] | 1675 | if (reallyUseQuotedPrintable) { |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1676 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1677 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 1678 | encodedValue = encodeQuotedPrintable(rawValue); |
| 1679 | } else { |
| 1680 | // TODO: one line may be too huge, which may be invalid in vCard spec, though |
| 1681 | // several (even well-known) applications do not care this. |
| 1682 | encodedValue = escapeCharacters(rawValue); |
| 1683 | } |
| 1684 | |
| 1685 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 1686 | mBuilder.append(encodedValue); |
| 1687 | mBuilder.append(VCARD_END_OF_LINE); |
| 1688 | } |
| 1689 | |
| 1690 | public void appendLine(final String propertyName, final List<String> rawValueList, |
| 1691 | final boolean needCharset, boolean needQuotedPrintable) { |
| 1692 | appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable); |
| 1693 | } |
| 1694 | |
| 1695 | public void appendLine(final String propertyName, final List<String> parameterList, |
| 1696 | final List<String> rawValueList, final boolean needCharset, |
| 1697 | final boolean needQuotedPrintable) { |
| 1698 | mBuilder.append(propertyName); |
| 1699 | if (parameterList != null && parameterList.size() > 0) { |
| 1700 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1701 | appendTypeParameters(parameterList); |
| 1702 | } |
| 1703 | if (needCharset) { |
| 1704 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1705 | mBuilder.append(mVCardCharsetParameter); |
| 1706 | } |
Daisuke Miyakawa | a750fdd | 2009-11-20 16:09:34 +0900 | [diff] [blame] | 1707 | if (needQuotedPrintable) { |
| 1708 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1709 | mBuilder.append(VCARD_PARAM_ENCODING_QP); |
| 1710 | } |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1711 | |
| 1712 | mBuilder.append(VCARD_DATA_SEPARATOR); |
| 1713 | boolean first = true; |
| 1714 | for (String rawValue : rawValueList) { |
| 1715 | final String encodedValue; |
| 1716 | if (needQuotedPrintable) { |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1717 | encodedValue = encodeQuotedPrintable(rawValue); |
| 1718 | } else { |
| 1719 | // TODO: one line may be too huge, which may be invalid in vCard 3.0 |
| 1720 | // (which says "When generating a content line, lines longer than |
| 1721 | // 75 characters SHOULD be folded"), though several |
| 1722 | // (even well-known) applications do not care this. |
| 1723 | encodedValue = escapeCharacters(rawValue); |
| 1724 | } |
| 1725 | |
| 1726 | if (first) { |
| 1727 | first = false; |
| 1728 | } else { |
| 1729 | mBuilder.append(VCARD_ITEM_SEPARATOR); |
| 1730 | } |
| 1731 | mBuilder.append(encodedValue); |
| 1732 | } |
| 1733 | mBuilder.append(VCARD_END_OF_LINE); |
| 1734 | } |
| 1735 | |
| 1736 | /** |
| 1737 | * VCARD_PARAM_SEPARATOR must be appended before this method being called. |
| 1738 | */ |
| 1739 | private void appendTypeParameters(final List<String> types) { |
| 1740 | // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, |
| 1741 | // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. |
| 1742 | boolean first = true; |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 1743 | for (final String typeValue : types) { |
| 1744 | // Note: vCard 3.0 specifies the different type of acceptable type Strings, but |
| 1745 | // we don't emit that kind of vCard 3.0 specific type since there should be |
| 1746 | // high probabilyty in which external importers cannot understand them. |
| 1747 | // |
| 1748 | // e.g. TYPE="\u578B\u306B\u3087" (vCard 3.0 allows non-Ascii characters if they |
| 1749 | // are quoted.) |
| 1750 | if (!VCardUtils.isV21Word(typeValue)) { |
| 1751 | continue; |
| 1752 | } |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1753 | if (first) { |
| 1754 | first = false; |
| 1755 | } else { |
| 1756 | mBuilder.append(VCARD_PARAM_SEPARATOR); |
| 1757 | } |
Daisuke Miyakawa | c4b5171 | 2009-11-19 09:00:12 +0900 | [diff] [blame] | 1758 | appendTypeParameter(typeValue); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1759 | } |
| 1760 | } |
| 1761 | |
| 1762 | /** |
| 1763 | * VCARD_PARAM_SEPARATOR must be appended before this method being called. |
| 1764 | */ |
| 1765 | private void appendTypeParameter(final String type) { |
| 1766 | appendTypeParameter(mBuilder, type); |
| 1767 | } |
| 1768 | |
| 1769 | private void appendTypeParameter(final StringBuilder builder, final String type) { |
| 1770 | // Refrain from using appendType() so that "TYPE=" is not be appended when the |
| 1771 | // device is DoCoMo's (just for safety). |
| 1772 | // |
| 1773 | // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" |
| 1774 | if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) { |
| 1775 | builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL); |
| 1776 | } |
| 1777 | builder.append(type); |
| 1778 | } |
| 1779 | |
| 1780 | /** |
| 1781 | * Returns true when the property line should contain charset parameter |
| 1782 | * information. This method may return true even when vCard version is 3.0. |
| 1783 | * |
| 1784 | * Strictly, adding charset information is invalid in VCard 3.0. |
| 1785 | * However we'll add the info only when charset we use is not UTF-8 |
| 1786 | * in vCard 3.0 format, since parser side may be able to use the charset |
| 1787 | * via this field, though we may encounter another problem by adding it. |
| 1788 | * |
| 1789 | * e.g. Japanese mobile phones use Shift_Jis while RFC 2426 |
| 1790 | * recommends UTF-8. By adding this field, parsers may be able |
| 1791 | * to know this text is NOT UTF-8 but Shift_Jis. |
| 1792 | */ |
| 1793 | private boolean shouldAppendCharsetParam(String...propertyValueList) { |
| 1794 | if (!mShouldAppendCharsetParam) { |
| 1795 | return false; |
| 1796 | } |
| 1797 | for (String propertyValue : propertyValueList) { |
| 1798 | if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { |
| 1799 | return true; |
| 1800 | } |
| 1801 | } |
| 1802 | return false; |
| 1803 | } |
| 1804 | |
Daisuke Miyakawa | 839c036 | 2009-11-18 10:25:09 +0900 | [diff] [blame] | 1805 | private String encodeQuotedPrintable(final String str) { |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1806 | if (TextUtils.isEmpty(str)) { |
| 1807 | return ""; |
| 1808 | } |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1809 | |
Daisuke Miyakawa | 839c036 | 2009-11-18 10:25:09 +0900 | [diff] [blame] | 1810 | final StringBuilder builder = new StringBuilder(); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1811 | int index = 0; |
| 1812 | int lineCount = 0; |
| 1813 | byte[] strArray = null; |
| 1814 | |
| 1815 | try { |
| 1816 | strArray = str.getBytes(mCharsetString); |
| 1817 | } catch (UnsupportedEncodingException e) { |
| 1818 | Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. " |
| 1819 | + "Try default charset"); |
| 1820 | strArray = str.getBytes(); |
| 1821 | } |
| 1822 | while (index < strArray.length) { |
Daisuke Miyakawa | 839c036 | 2009-11-18 10:25:09 +0900 | [diff] [blame] | 1823 | builder.append(String.format("=%02X", strArray[index])); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1824 | index += 1; |
| 1825 | lineCount += 3; |
| 1826 | |
| 1827 | if (lineCount >= 67) { |
| 1828 | // Specification requires CRLF must be inserted before the |
| 1829 | // length of the line |
| 1830 | // becomes more than 76. |
| 1831 | // Assuming that the next character is a multi-byte character, |
| 1832 | // it will become |
| 1833 | // 6 bytes. |
| 1834 | // 76 - 6 - 3 = 67 |
Daisuke Miyakawa | 839c036 | 2009-11-18 10:25:09 +0900 | [diff] [blame] | 1835 | builder.append("=\r\n"); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1836 | lineCount = 0; |
| 1837 | } |
| 1838 | } |
| 1839 | |
Daisuke Miyakawa | 839c036 | 2009-11-18 10:25:09 +0900 | [diff] [blame] | 1840 | return builder.toString(); |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1841 | } |
| 1842 | |
Daisuke Miyakawa | 1b9e2be | 2009-11-17 11:40:45 +0900 | [diff] [blame] | 1843 | /** |
| 1844 | * Append '\' to the characters which should be escaped. The character set is different |
| 1845 | * not only between vCard 2.1 and vCard 3.0 but also among each device. |
| 1846 | * |
| 1847 | * Note that Quoted-Printable string must not be input here. |
| 1848 | */ |
| 1849 | @SuppressWarnings("fallthrough") |
| 1850 | private String escapeCharacters(final String unescaped) { |
| 1851 | if (TextUtils.isEmpty(unescaped)) { |
| 1852 | return ""; |
| 1853 | } |
| 1854 | |
| 1855 | final StringBuilder tmpBuilder = new StringBuilder(); |
| 1856 | final int length = unescaped.length(); |
| 1857 | for (int i = 0; i < length; i++) { |
| 1858 | final char ch = unescaped.charAt(i); |
| 1859 | switch (ch) { |
| 1860 | case ';': { |
| 1861 | tmpBuilder.append('\\'); |
| 1862 | tmpBuilder.append(';'); |
| 1863 | break; |
| 1864 | } |
| 1865 | case '\r': { |
| 1866 | if (i + 1 < length) { |
| 1867 | char nextChar = unescaped.charAt(i); |
| 1868 | if (nextChar == '\n') { |
| 1869 | break; |
| 1870 | } else { |
| 1871 | // fall through |
| 1872 | } |
| 1873 | } else { |
| 1874 | // fall through |
| 1875 | } |
| 1876 | } |
| 1877 | case '\n': { |
| 1878 | // In vCard 2.1, there's no specification about this, while |
| 1879 | // vCard 3.0 explicitly requires this should be encoded to "\n". |
| 1880 | tmpBuilder.append("\\n"); |
| 1881 | break; |
| 1882 | } |
| 1883 | case '\\': { |
| 1884 | if (mIsV30) { |
| 1885 | tmpBuilder.append("\\\\"); |
| 1886 | break; |
| 1887 | } else { |
| 1888 | // fall through |
| 1889 | } |
| 1890 | } |
| 1891 | case '<': |
| 1892 | case '>': { |
| 1893 | if (mIsDoCoMo) { |
| 1894 | tmpBuilder.append('\\'); |
| 1895 | tmpBuilder.append(ch); |
| 1896 | } else { |
| 1897 | tmpBuilder.append(ch); |
| 1898 | } |
| 1899 | break; |
| 1900 | } |
| 1901 | case ',': { |
| 1902 | if (mIsV30) { |
| 1903 | tmpBuilder.append("\\,"); |
| 1904 | } else { |
| 1905 | tmpBuilder.append(ch); |
| 1906 | } |
| 1907 | break; |
| 1908 | } |
| 1909 | default: { |
| 1910 | tmpBuilder.append(ch); |
| 1911 | break; |
| 1912 | } |
| 1913 | } |
| 1914 | } |
| 1915 | return tmpBuilder.toString(); |
| 1916 | } |
| 1917 | |
| 1918 | @Override |
| 1919 | public String toString() { |
| 1920 | if (!mEndAppended) { |
| 1921 | if (mIsDoCoMo) { |
| 1922 | appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); |
| 1923 | appendLine(VCardConstants.PROPERTY_X_REDUCTION, ""); |
| 1924 | appendLine(VCardConstants.PROPERTY_X_NO, ""); |
| 1925 | appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, ""); |
| 1926 | } |
| 1927 | appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD); |
| 1928 | mEndAppended = true; |
| 1929 | } |
| 1930 | return mBuilder.toString(); |
| 1931 | } |
| 1932 | } |