blob: d634672b74cece38c2fb698ab07349f0ef60a473 [file] [log] [blame]
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001/*
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 */
16package android.pim.vcard;
17
18import android.content.ContentValues;
19import android.provider.ContactsContract.CommonDataKinds.Email;
20import android.provider.ContactsContract.CommonDataKinds.Event;
21import android.provider.ContactsContract.CommonDataKinds.Im;
22import android.provider.ContactsContract.CommonDataKinds.Nickname;
23import android.provider.ContactsContract.CommonDataKinds.Note;
24import android.provider.ContactsContract.CommonDataKinds.Organization;
25import android.provider.ContactsContract.CommonDataKinds.Phone;
26import android.provider.ContactsContract.CommonDataKinds.Photo;
27import android.provider.ContactsContract.CommonDataKinds.Relation;
28import android.provider.ContactsContract.CommonDataKinds.StructuredName;
29import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
30import android.provider.ContactsContract.CommonDataKinds.Website;
31import android.telephony.PhoneNumberUtils;
32import android.text.TextUtils;
33import android.util.CharsetUtils;
34import android.util.Log;
35
36import org.apache.commons.codec.binary.Base64;
37
38import java.io.UnsupportedEncodingException;
39import java.nio.charset.UnsupportedCharsetException;
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.Collections;
43import java.util.HashMap;
44import java.util.HashSet;
45import java.util.List;
46import java.util.Map;
47import java.util.Set;
48
49/**
50 * The class which lets users create their own vCard String.
51 */
52public 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 Miyakawad2145b92009-11-17 17:03:23 +090092 private final boolean mShouldUseQuotedPrintable;
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +090093 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 Miyakawa49c0dec2009-11-18 17:11:43 +090098 private final boolean mRefrainsQPToNameProperties;
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +090099 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 Miyakawad2145b92009-11-17 17:03:23 +0900113 mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900114 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 Miyakawa49c0dec2009-11-18 17:11:43 +0900121 mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900122 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 Miyakawa49c0dec2009-11-18 17:11:43 +0900258 (!mRefrainsQPToNameProperties &&
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900259 !(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 Miyakawa49c0dec2009-11-18 17:11:43 +0900276 !mRefrainsQPToNameProperties &&
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900277 !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 Miyakawa49c0dec2009-11-18 17:11:43 +0900356 (!mRefrainsQPToNameProperties &&
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900357 !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 Miyakawa49c0dec2009-11-18 17:11:43 +0900474 (!mRefrainsQPToNameProperties
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900475 && !(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 Miyakawad2145b92009-11-17 17:03:23 +0900532 (mShouldUseQuotedPrintable &&
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900533 !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 Miyakawad2145b92009-11-17 17:03:23 +0900555 (mShouldUseQuotedPrintable &&
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900556 !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 Miyakawad2145b92009-11-17 17:03:23 +0900578 (mShouldUseQuotedPrintable &&
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900579 !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 Miyakawaba2593a2010-04-07 17:07:57 +0900645
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 Miyakawa1b9e2be2009-11-17 11:40:45 +0900650 phoneLineExists = true;
651 if (!phoneSet.contains(phoneNumber)) {
652 phoneSet.add(phoneNumber);
653 appendTelLine(type, label, phoneNumber, isPrimary);
654 }
655 } else {
Daisuke Miyakawaba2593a2010-04-07 17:07:57 +0900656 final List<String> phoneNumberList = splitAndTrimPhoneNumbers(phoneNumber);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900657 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 Miyakawaba2593a2010-04-07 17:07:57 +0900669 } // for (String actualPhoneNumber : phoneNumberList) {
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900670 }
671 }
672 }
673
674 if (!phoneLineExists && mIsDoCoMo) {
675 appendTelLine(Phone.TYPE_HOME, "", "", false);
676 }
677
678 return this;
679 }
680
Daisuke Miyakawaba2593a2010-04-07 17:07:57 +0900681 /**
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 Miyakawa1b9e2be2009-11-17 11:40:45 +0900708
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 Miyakawa900731de2010-02-03 12:42:26 -0800713 if (Character.isDigit(ch) || ch == '+') {
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900714 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 Miyakawac4b51712009-11-19 09:00:12 +0900859 // 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 Miyakawa639b0f02009-11-20 07:06:40 +0900863 final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD);
Daisuke Miyakawac4b51712009-11-19 09:00:12 +0900864 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 Miyakawa639b0f02009-11-20 07:06:40 +0900870 rawPoBox, rawNeighborhood, rawStreet, rawLocality,
Daisuke Miyakawac4b51712009-11-19 09:00:12 +0900871 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 Miyakawac4b51712009-11-19 09:00:12 +0900879 final String encodedStreet;
880 final String encodedLocality;
881 final String encodedRegion;
882 final String encodedPostalCode;
883 final String encodedCountry;
Daisuke Miyakawa639b0f02009-11-20 07:06:40 +0900884 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 Miyakawa1b9e2be2009-11-17 11:40:45 +0900909 if (reallyUseQuotedPrintable) {
Daisuke Miyakawac4b51712009-11-19 09:00:12 +0900910 encodedPoBox = encodeQuotedPrintable(rawPoBox);
Daisuke Miyakawac4b51712009-11-19 09:00:12 +0900911 encodedStreet = encodeQuotedPrintable(rawStreet);
Daisuke Miyakawa639b0f02009-11-20 07:06:40 +0900912 encodedLocality = encodeQuotedPrintable(rawLocality2);
Daisuke Miyakawac4b51712009-11-19 09:00:12 +0900913 encodedRegion = encodeQuotedPrintable(rawRegion);
914 encodedPostalCode = encodeQuotedPrintable(rawPostalCode);
915 encodedCountry = encodeQuotedPrintable(rawCountry);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900916 } else {
Daisuke Miyakawac4b51712009-11-19 09:00:12 +0900917 encodedPoBox = escapeCharacters(rawPoBox);
Daisuke Miyakawac4b51712009-11-19 09:00:12 +0900918 encodedStreet = escapeCharacters(rawStreet);
Daisuke Miyakawa639b0f02009-11-20 07:06:40 +0900919 encodedLocality = escapeCharacters(rawLocality2);
Daisuke Miyakawac4b51712009-11-19 09:00:12 +0900920 encodedRegion = escapeCharacters(rawRegion);
921 encodedPostalCode = escapeCharacters(rawPostalCode);
922 encodedCountry = escapeCharacters(rawCountry);
Daisuke Miyakawa639b0f02009-11-20 07:06:40 +0900923 encodedNeighborhood = escapeCharacters(rawNeighborhood);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900924 }
Daisuke Miyakawac4b51712009-11-19 09:00:12 +0900925 final StringBuffer addressBuffer = new StringBuffer();
926 addressBuffer.append(encodedPoBox);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +0900927 addressBuffer.append(VCARD_ITEM_SEPARATOR);
Daisuke Miyakawac4b51712009-11-19 09:00:12 +0900928 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 Miyakawa1b9e2be2009-11-17 11:40:45 +0900965 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 Miyakawa1b9e2be2009-11-17 11:40:45 +0900973 }
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 Miyakawa1b9e2be2009-11-17 11:40:45 +09001045 if (!TextUtils.isEmpty(website)) {
Daisuke Miyakawa49c0dec2009-11-18 17:11:43 +09001046 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001047 }
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 Miyakawad2145b92009-11-17 17:03:23 +09001082 (mShouldUseQuotedPrintable &&
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001083 !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline)));
1084
1085 if (!TextUtils.isEmpty(title)) {
1086 appendLine(VCardConstants.PROPERTY_TITLE, title,
1087 !VCardUtils.containsOnlyPrintableAscii(title),
Daisuke Miyakawad2145b92009-11-17 17:03:23 +09001088 (mShouldUseQuotedPrintable &&
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001089 !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 Miyakawa49c0dec2009-11-18 17:11:43 +09001106 final String photoType = VCardUtils.guessImageType(data);
1107 if (photoType == null) {
1108 Log.d(LOG_TAG, "Unknown photo type. Ignored.");
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001109 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 Miyakawa839c0362009-11-18 10:25:09 +09001123 final StringBuilder noteBuilder = new StringBuilder();
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001124 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 Miyakawad2145b92009-11-17 17:03:23 +09001145 (mShouldUseQuotedPrintable &&
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001146 !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 Miyakawad2145b92009-11-17 17:03:23 +09001156 (mShouldUseQuotedPrintable &&
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001157 !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);
Daisuke Miyakawab7688552010-08-17 16:33:38 -07001466 } else if (mIsV30) {
1467 // This label is appropriately encoded in appendTypeParameters.
1468 parameterList.add(label);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001469 } else {
1470 final String upperLabel = label.toUpperCase();
1471 if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) {
1472 parameterList.add(upperLabel);
1473 } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
1474 // Note: Strictly, vCard 2.1 does not allow "X-" parameter without
1475 // "TYPE=" string.
1476 parameterList.add("X-" + label);
1477 }
1478 }
1479 break;
1480 }
1481 case Phone.TYPE_RADIO:
1482 case Phone.TYPE_TTY_TDD:
1483 default: {
1484 break;
1485 }
1486 }
1487
1488 if (isPrimary) {
1489 parameterList.add(VCardConstants.PARAM_TYPE_PREF);
1490 }
1491
1492 if (parameterList.isEmpty()) {
1493 appendUncommonPhoneType(mBuilder, type);
1494 } else {
1495 appendTypeParameters(parameterList);
1496 }
1497
1498 mBuilder.append(VCARD_DATA_SEPARATOR);
1499 mBuilder.append(encodedValue);
1500 mBuilder.append(VCARD_END_OF_LINE);
1501 }
1502
1503 /**
1504 * Appends phone type string which may not be available in some devices.
1505 */
1506 private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) {
1507 if (mIsDoCoMo) {
1508 // The previous implementation for DoCoMo had been conservative
1509 // about miscellaneous types.
1510 builder.append(VCardConstants.PARAM_TYPE_VOICE);
1511 } else {
1512 String phoneType = VCardUtils.getPhoneTypeString(type);
1513 if (phoneType != null) {
1514 appendTypeParameter(phoneType);
1515 } else {
1516 Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type);
1517 }
1518 }
1519 }
1520
1521 /**
1522 * @param encodedValue Must be encoded by BASE64
1523 * @param photoType
1524 */
1525 public void appendPhotoLine(final String encodedValue, final String photoType) {
1526 StringBuilder tmpBuilder = new StringBuilder();
1527 tmpBuilder.append(VCardConstants.PROPERTY_PHOTO);
1528 tmpBuilder.append(VCARD_PARAM_SEPARATOR);
1529 if (mIsV30) {
1530 tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30);
1531 } else {
1532 tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21);
1533 }
1534 tmpBuilder.append(VCARD_PARAM_SEPARATOR);
1535 appendTypeParameter(tmpBuilder, photoType);
1536 tmpBuilder.append(VCARD_DATA_SEPARATOR);
1537 tmpBuilder.append(encodedValue);
1538
1539 final String tmpStr = tmpBuilder.toString();
1540 tmpBuilder = new StringBuilder();
1541 int lineCount = 0;
1542 final int length = tmpStr.length();
1543 final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30
1544 - VCARD_END_OF_LINE.length();
1545 final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length();
1546 int maxNum = maxNumForFirstLine;
1547 for (int i = 0; i < length; i++) {
1548 tmpBuilder.append(tmpStr.charAt(i));
1549 lineCount++;
1550 if (lineCount > maxNum) {
1551 tmpBuilder.append(VCARD_END_OF_LINE);
1552 tmpBuilder.append(VCARD_WS);
1553 maxNum = maxNumInGeneral;
1554 lineCount = 0;
1555 }
1556 }
1557 mBuilder.append(tmpBuilder.toString());
1558 mBuilder.append(VCARD_END_OF_LINE);
1559 mBuilder.append(VCARD_END_OF_LINE);
1560 }
1561
1562 public void appendAndroidSpecificProperty(final String mimeType, ContentValues contentValues) {
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001563 if (!sAllowedAndroidPropertySet.contains(mimeType)) {
1564 return;
1565 }
Daisuke Miyakawaa750fdd2009-11-20 16:09:34 +09001566 final List<String> rawValueList = new ArrayList<String>();
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001567 for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) {
1568 String value = contentValues.getAsString("data" + i);
1569 if (value == null) {
1570 value = "";
1571 }
1572 rawValueList.add(value);
1573 }
1574
Daisuke Miyakawaa750fdd2009-11-20 16:09:34 +09001575 boolean needCharset =
1576 (mShouldAppendCharsetParam &&
1577 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1578 boolean reallyUseQuotedPrintable =
1579 (mShouldUseQuotedPrintable &&
1580 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1581 mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM);
1582 if (needCharset) {
1583 mBuilder.append(VCARD_PARAM_SEPARATOR);
1584 mBuilder.append(mVCardCharsetParameter);
1585 }
1586 if (reallyUseQuotedPrintable) {
1587 mBuilder.append(VCARD_PARAM_SEPARATOR);
1588 mBuilder.append(VCARD_PARAM_ENCODING_QP);
1589 }
1590 mBuilder.append(VCARD_DATA_SEPARATOR);
1591 mBuilder.append(mimeType); // Should not be encoded.
1592 for (String rawValue : rawValueList) {
1593 final String encodedValue;
1594 if (reallyUseQuotedPrintable) {
1595 encodedValue = encodeQuotedPrintable(rawValue);
1596 } else {
1597 // TODO: one line may be too huge, which may be invalid in vCard 3.0
1598 // (which says "When generating a content line, lines longer than
1599 // 75 characters SHOULD be folded"), though several
1600 // (even well-known) applications do not care this.
1601 encodedValue = escapeCharacters(rawValue);
1602 }
1603 mBuilder.append(VCARD_ITEM_SEPARATOR);
1604 mBuilder.append(encodedValue);
1605 }
1606 mBuilder.append(VCARD_END_OF_LINE);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001607 }
1608
1609 public void appendLineWithCharsetAndQPDetection(final String propertyName,
1610 final String rawValue) {
1611 appendLineWithCharsetAndQPDetection(propertyName, null, rawValue);
1612 }
1613
Daisuke Miyakawaa750fdd2009-11-20 16:09:34 +09001614 public void appendLineWithCharsetAndQPDetection(
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001615 final String propertyName, final List<String> rawValueList) {
1616 appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList);
1617 }
1618
1619 public void appendLineWithCharsetAndQPDetection(final String propertyName,
1620 final List<String> parameterList, final String rawValue) {
1621 final boolean needCharset =
Daisuke Miyakawad2145b92009-11-17 17:03:23 +09001622 !VCardUtils.containsOnlyPrintableAscii(rawValue);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001623 final boolean reallyUseQuotedPrintable =
Daisuke Miyakawad2145b92009-11-17 17:03:23 +09001624 (mShouldUseQuotedPrintable &&
1625 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue));
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001626 appendLine(propertyName, parameterList,
1627 rawValue, needCharset, reallyUseQuotedPrintable);
1628 }
1629
1630 public void appendLineWithCharsetAndQPDetection(final String propertyName,
1631 final List<String> parameterList, final List<String> rawValueList) {
Daisuke Miyakawaa750fdd2009-11-20 16:09:34 +09001632 boolean needCharset =
1633 (mShouldAppendCharsetParam &&
1634 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1635 boolean reallyUseQuotedPrintable =
1636 (mShouldUseQuotedPrintable &&
1637 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001638 appendLine(propertyName, parameterList, rawValueList,
1639 needCharset, reallyUseQuotedPrintable);
1640 }
1641
1642 /**
1643 * Appends one line with a given property name and value.
1644 */
1645 public void appendLine(final String propertyName, final String rawValue) {
1646 appendLine(propertyName, rawValue, false, false);
1647 }
1648
1649 public void appendLine(final String propertyName, final List<String> rawValueList) {
1650 appendLine(propertyName, rawValueList, false, false);
1651 }
1652
1653 public void appendLine(final String propertyName,
Daisuke Miyakawaa750fdd2009-11-20 16:09:34 +09001654 final String rawValue, final boolean needCharset,
1655 boolean reallyUseQuotedPrintable) {
1656 appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable);
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001657 }
1658
1659 public void appendLine(final String propertyName, final List<String> parameterList,
1660 final String rawValue) {
1661 appendLine(propertyName, parameterList, rawValue, false, false);
1662 }
1663
1664 public void appendLine(final String propertyName, final List<String> parameterList,
Daisuke Miyakawaa750fdd2009-11-20 16:09:34 +09001665 final String rawValue, final boolean needCharset,
1666 boolean reallyUseQuotedPrintable) {
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001667 mBuilder.append(propertyName);
1668 if (parameterList != null && parameterList.size() > 0) {
1669 mBuilder.append(VCARD_PARAM_SEPARATOR);
1670 appendTypeParameters(parameterList);
1671 }
1672 if (needCharset) {
1673 mBuilder.append(VCARD_PARAM_SEPARATOR);
1674 mBuilder.append(mVCardCharsetParameter);
1675 }
1676
1677 final String encodedValue;
Daisuke Miyakawaa750fdd2009-11-20 16:09:34 +09001678 if (reallyUseQuotedPrintable) {
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001679 mBuilder.append(VCARD_PARAM_SEPARATOR);
1680 mBuilder.append(VCARD_PARAM_ENCODING_QP);
1681 encodedValue = encodeQuotedPrintable(rawValue);
1682 } else {
1683 // TODO: one line may be too huge, which may be invalid in vCard spec, though
1684 // several (even well-known) applications do not care this.
1685 encodedValue = escapeCharacters(rawValue);
1686 }
1687
1688 mBuilder.append(VCARD_DATA_SEPARATOR);
1689 mBuilder.append(encodedValue);
1690 mBuilder.append(VCARD_END_OF_LINE);
1691 }
1692
1693 public void appendLine(final String propertyName, final List<String> rawValueList,
1694 final boolean needCharset, boolean needQuotedPrintable) {
1695 appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable);
1696 }
1697
1698 public void appendLine(final String propertyName, final List<String> parameterList,
1699 final List<String> rawValueList, final boolean needCharset,
1700 final boolean needQuotedPrintable) {
1701 mBuilder.append(propertyName);
1702 if (parameterList != null && parameterList.size() > 0) {
1703 mBuilder.append(VCARD_PARAM_SEPARATOR);
1704 appendTypeParameters(parameterList);
1705 }
1706 if (needCharset) {
1707 mBuilder.append(VCARD_PARAM_SEPARATOR);
1708 mBuilder.append(mVCardCharsetParameter);
1709 }
Daisuke Miyakawaa750fdd2009-11-20 16:09:34 +09001710 if (needQuotedPrintable) {
1711 mBuilder.append(VCARD_PARAM_SEPARATOR);
1712 mBuilder.append(VCARD_PARAM_ENCODING_QP);
1713 }
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001714
1715 mBuilder.append(VCARD_DATA_SEPARATOR);
1716 boolean first = true;
1717 for (String rawValue : rawValueList) {
1718 final String encodedValue;
1719 if (needQuotedPrintable) {
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001720 encodedValue = encodeQuotedPrintable(rawValue);
1721 } else {
1722 // TODO: one line may be too huge, which may be invalid in vCard 3.0
1723 // (which says "When generating a content line, lines longer than
1724 // 75 characters SHOULD be folded"), though several
1725 // (even well-known) applications do not care this.
1726 encodedValue = escapeCharacters(rawValue);
1727 }
1728
1729 if (first) {
1730 first = false;
1731 } else {
1732 mBuilder.append(VCARD_ITEM_SEPARATOR);
1733 }
1734 mBuilder.append(encodedValue);
1735 }
1736 mBuilder.append(VCARD_END_OF_LINE);
1737 }
1738
1739 /**
1740 * VCARD_PARAM_SEPARATOR must be appended before this method being called.
1741 */
1742 private void appendTypeParameters(final List<String> types) {
1743 // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
1744 // which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
1745 boolean first = true;
Daisuke Miyakawac4b51712009-11-19 09:00:12 +09001746 for (final String typeValue : types) {
Daisuke Miyakawab7688552010-08-17 16:33:38 -07001747 if (VCardConfig.isV30(mVCardType)) {
1748 // Note: vCard 3.0 specifies the different type of acceptable type Strings, but
1749 // we don't emit that kind of vCard 3.0 specific type since there should be
1750 // high probabilyty in which external importers cannot understand them.
1751 //
1752 // e.g. TYPE="\u578B\u306B\u3087" (vCard 3.0 allows non-Ascii characters if they
1753 // are quoted.)
1754 if (first) {
1755 first = false;
1756 } else {
1757 mBuilder.append(VCARD_PARAM_SEPARATOR);
1758 }
1759 appendTypeParameter(VCardUtils.toStringAvailableAsV30ParameValue(typeValue));
1760 } else { // vCard 2.1
1761 if (!VCardUtils.isV21Word(typeValue)) {
1762 continue;
1763 }
1764 if (first) {
1765 first = false;
1766 } else {
1767 mBuilder.append(VCARD_PARAM_SEPARATOR);
1768 }
1769 appendTypeParameter(typeValue);
Daisuke Miyakawac4b51712009-11-19 09:00:12 +09001770 }
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001771 }
1772 }
1773
1774 /**
1775 * VCARD_PARAM_SEPARATOR must be appended before this method being called.
1776 */
1777 private void appendTypeParameter(final String type) {
1778 appendTypeParameter(mBuilder, type);
1779 }
1780
1781 private void appendTypeParameter(final StringBuilder builder, final String type) {
1782 // Refrain from using appendType() so that "TYPE=" is not be appended when the
1783 // device is DoCoMo's (just for safety).
1784 //
1785 // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
1786 if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) {
1787 builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
1788 }
1789 builder.append(type);
1790 }
1791
1792 /**
1793 * Returns true when the property line should contain charset parameter
1794 * information. This method may return true even when vCard version is 3.0.
1795 *
1796 * Strictly, adding charset information is invalid in VCard 3.0.
1797 * However we'll add the info only when charset we use is not UTF-8
1798 * in vCard 3.0 format, since parser side may be able to use the charset
1799 * via this field, though we may encounter another problem by adding it.
1800 *
1801 * e.g. Japanese mobile phones use Shift_Jis while RFC 2426
1802 * recommends UTF-8. By adding this field, parsers may be able
1803 * to know this text is NOT UTF-8 but Shift_Jis.
1804 */
1805 private boolean shouldAppendCharsetParam(String...propertyValueList) {
1806 if (!mShouldAppendCharsetParam) {
1807 return false;
1808 }
1809 for (String propertyValue : propertyValueList) {
1810 if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
1811 return true;
1812 }
1813 }
1814 return false;
1815 }
1816
Daisuke Miyakawa839c0362009-11-18 10:25:09 +09001817 private String encodeQuotedPrintable(final String str) {
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001818 if (TextUtils.isEmpty(str)) {
1819 return "";
1820 }
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001821
Daisuke Miyakawa839c0362009-11-18 10:25:09 +09001822 final StringBuilder builder = new StringBuilder();
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001823 int index = 0;
1824 int lineCount = 0;
1825 byte[] strArray = null;
1826
1827 try {
1828 strArray = str.getBytes(mCharsetString);
1829 } catch (UnsupportedEncodingException e) {
1830 Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. "
1831 + "Try default charset");
1832 strArray = str.getBytes();
1833 }
1834 while (index < strArray.length) {
Daisuke Miyakawa839c0362009-11-18 10:25:09 +09001835 builder.append(String.format("=%02X", strArray[index]));
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001836 index += 1;
1837 lineCount += 3;
1838
1839 if (lineCount >= 67) {
1840 // Specification requires CRLF must be inserted before the
1841 // length of the line
1842 // becomes more than 76.
1843 // Assuming that the next character is a multi-byte character,
1844 // it will become
1845 // 6 bytes.
1846 // 76 - 6 - 3 = 67
Daisuke Miyakawa839c0362009-11-18 10:25:09 +09001847 builder.append("=\r\n");
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001848 lineCount = 0;
1849 }
1850 }
1851
Daisuke Miyakawa839c0362009-11-18 10:25:09 +09001852 return builder.toString();
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001853 }
1854
Daisuke Miyakawa1b9e2be2009-11-17 11:40:45 +09001855 /**
1856 * Append '\' to the characters which should be escaped. The character set is different
1857 * not only between vCard 2.1 and vCard 3.0 but also among each device.
1858 *
1859 * Note that Quoted-Printable string must not be input here.
1860 */
1861 @SuppressWarnings("fallthrough")
1862 private String escapeCharacters(final String unescaped) {
1863 if (TextUtils.isEmpty(unescaped)) {
1864 return "";
1865 }
1866
1867 final StringBuilder tmpBuilder = new StringBuilder();
1868 final int length = unescaped.length();
1869 for (int i = 0; i < length; i++) {
1870 final char ch = unescaped.charAt(i);
1871 switch (ch) {
1872 case ';': {
1873 tmpBuilder.append('\\');
1874 tmpBuilder.append(';');
1875 break;
1876 }
1877 case '\r': {
1878 if (i + 1 < length) {
1879 char nextChar = unescaped.charAt(i);
1880 if (nextChar == '\n') {
1881 break;
1882 } else {
1883 // fall through
1884 }
1885 } else {
1886 // fall through
1887 }
1888 }
1889 case '\n': {
1890 // In vCard 2.1, there's no specification about this, while
1891 // vCard 3.0 explicitly requires this should be encoded to "\n".
1892 tmpBuilder.append("\\n");
1893 break;
1894 }
1895 case '\\': {
1896 if (mIsV30) {
1897 tmpBuilder.append("\\\\");
1898 break;
1899 } else {
1900 // fall through
1901 }
1902 }
1903 case '<':
1904 case '>': {
1905 if (mIsDoCoMo) {
1906 tmpBuilder.append('\\');
1907 tmpBuilder.append(ch);
1908 } else {
1909 tmpBuilder.append(ch);
1910 }
1911 break;
1912 }
1913 case ',': {
1914 if (mIsV30) {
1915 tmpBuilder.append("\\,");
1916 } else {
1917 tmpBuilder.append(ch);
1918 }
1919 break;
1920 }
1921 default: {
1922 tmpBuilder.append(ch);
1923 break;
1924 }
1925 }
1926 }
1927 return tmpBuilder.toString();
1928 }
1929
1930 @Override
1931 public String toString() {
1932 if (!mEndAppended) {
1933 if (mIsDoCoMo) {
1934 appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
1935 appendLine(VCardConstants.PROPERTY_X_REDUCTION, "");
1936 appendLine(VCardConstants.PROPERTY_X_NO, "");
1937 appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, "");
1938 }
1939 appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD);
1940 mEndAppended = true;
1941 }
1942 return mBuilder.toString();
1943 }
1944}