blob: d2733475858cc9c3dbb61ba96da2da9f7781c849 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.android.vcard;
import android.content.ContentValues;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.Relation;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import com.android.vcard.VCardUtils.PhoneNumberUtilsPort;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* <p>
* The class which lets users create their own vCard String. Typical usage is as follows:
* </p>
* <pre class="prettyprint">final VCardBuilder builder = new VCardBuilder(vcardType);
* builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
* .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
* .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
* .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
* .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
* .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
* .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE))
* .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE))
* .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
* .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
* .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
* .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
* return builder.toString();</pre>
*/
public class VCardBuilder {
private static final String LOG_TAG = VCardConstants.LOG_TAG;
// If you add the other element, please check all the columns are able to be
// converted to String.
//
// e.g. BLOB is not what we can handle here now.
private static final Set<String> sAllowedAndroidPropertySet =
Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE,
Relation.CONTENT_ITEM_TYPE)));
public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME;
public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME;
public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER;
private static final String VCARD_DATA_VCARD = "VCARD";
private static final String VCARD_DATA_PUBLIC = "PUBLIC";
private static final String VCARD_PARAM_SEPARATOR = ";";
public static final String VCARD_END_OF_LINE = "\r\n";
private static final String VCARD_DATA_SEPARATOR = ":";
private static final String VCARD_ITEM_SEPARATOR = ";";
private static final String VCARD_WS = " ";
private static final String VCARD_PARAM_EQUAL = "=";
private static final String VCARD_PARAM_ENCODING_QP =
"ENCODING=" + VCardConstants.PARAM_ENCODING_QP;
private static final String VCARD_PARAM_ENCODING_BASE64_V21 =
"ENCODING=" + VCardConstants.PARAM_ENCODING_BASE64;
private static final String VCARD_PARAM_ENCODING_BASE64_AS_B =
"ENCODING=" + VCardConstants.PARAM_ENCODING_B;
private static final String SHIFT_JIS = "SHIFT_JIS";
private final int mVCardType;
private final boolean mIsV30OrV40;
private final boolean mIsJapaneseMobilePhone;
private final boolean mOnlyOneNoteFieldIsAvailable;
private final boolean mIsDoCoMo;
private final boolean mShouldUseQuotedPrintable;
private final boolean mUsesAndroidProperty;
private final boolean mUsesDefactProperty;
private final boolean mAppendTypeParamName;
private final boolean mRefrainsQPToNameProperties;
private final boolean mNeedsToConvertPhoneticString;
private final boolean mShouldAppendCharsetParam;
private final String mCharset;
private final String mVCardCharsetParameter;
private StringBuilder mBuilder;
private boolean mEndAppended;
public VCardBuilder(final int vcardType) {
// Default charset should be used
this(vcardType, null);
}
/**
* @param vcardType
* @param charset If null, we use default charset for export.
* @hide
*/
public VCardBuilder(final int vcardType, String charset) {
mVCardType = vcardType;
if (VCardConfig.isVersion40(vcardType)) {
Log.w(LOG_TAG, "Should not use vCard 4.0 when building vCard. " +
"It is not officially published yet.");
}
mIsV30OrV40 = VCardConfig.isVersion30(vcardType) || VCardConfig.isVersion40(vcardType);
mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType);
mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType);
mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType);
mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType);
mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType);
mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType);
mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType);
mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType);
// vCard 2.1 requires charset.
// vCard 3.0 does not allow it but we found some devices use it to determine
// the exact charset.
// We currently append it only when charset other than UTF_8 is used.
mShouldAppendCharsetParam =
!(VCardConfig.isVersion30(vcardType) && "UTF-8".equalsIgnoreCase(charset));
if (VCardConfig.isDoCoMo(vcardType)) {
if (!SHIFT_JIS.equalsIgnoreCase(charset)) {
/* Log.w(LOG_TAG,
"The charset \"" + charset + "\" is used while "
+ SHIFT_JIS + " is needed to be used."); */
if (TextUtils.isEmpty(charset)) {
mCharset = SHIFT_JIS;
} else {
mCharset = charset;
}
} else {
mCharset = charset;
}
mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
} else {
if (TextUtils.isEmpty(charset)) {
Log.i(LOG_TAG,
"Use the charset \"" + VCardConfig.DEFAULT_EXPORT_CHARSET
+ "\" for export.");
mCharset = VCardConfig.DEFAULT_EXPORT_CHARSET;
mVCardCharsetParameter = "CHARSET=" + VCardConfig.DEFAULT_EXPORT_CHARSET;
} else {
mCharset = charset;
mVCardCharsetParameter = "CHARSET=" + charset;
}
}
clear();
}
public void clear() {
mBuilder = new StringBuilder();
mEndAppended = false;
appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD);
if (VCardConfig.isVersion40(mVCardType)) {
appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V40);
} else if (VCardConfig.isVersion30(mVCardType)) {
appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30);
} else {
if (!VCardConfig.isVersion21(mVCardType)) {
Log.w(LOG_TAG, "Unknown vCard version detected.");
}
appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21);
}
}
private boolean containsNonEmptyName(final ContentValues contentValues) {
final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
final String prefix = contentValues.getAsString(StructuredName.PREFIX);
final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
final String phoneticFamilyName =
contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
final String phoneticMiddleName =
contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
final String phoneticGivenName =
contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) &&
TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) &&
TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) &&
TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) &&
TextUtils.isEmpty(displayName));
}
private ContentValues getPrimaryContentValueWithStructuredName(
final List<ContentValues> contentValuesList) {
ContentValues primaryContentValues = null;
ContentValues subprimaryContentValues = null;
for (ContentValues contentValues : contentValuesList) {
if (contentValues == null){
continue;
}
Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY);
if (isSuperPrimary != null && isSuperPrimary > 0) {
// We choose "super primary" ContentValues.
primaryContentValues = contentValues;
break;
} else if (primaryContentValues == null) {
// We choose the first "primary" ContentValues
// if "super primary" ContentValues does not exist.
final Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY);
if (isPrimary != null && isPrimary > 0 &&
containsNonEmptyName(contentValues)) {
primaryContentValues = contentValues;
// Do not break, since there may be ContentValues with "super primary"
// afterword.
} else if (subprimaryContentValues == null &&
containsNonEmptyName(contentValues)) {
subprimaryContentValues = contentValues;
}
}
}
if (primaryContentValues == null) {
if (subprimaryContentValues != null) {
// We choose the first ContentValues if any "primary" ContentValues does not exist.
primaryContentValues = subprimaryContentValues;
} else {
// There's no appropriate ContentValue with StructuredName.
primaryContentValues = new ContentValues();
}
}
return primaryContentValues;
}
/**
* To avoid unnecessary complication in logic, we use this method to construct N, FN
* properties for vCard 4.0.
*/
private VCardBuilder appendNamePropertiesV40(final List<ContentValues> contentValuesList) {
if (mIsDoCoMo || mNeedsToConvertPhoneticString) {
// Ignore all flags that look stale from the view of vCard 4.0 to
// simplify construction algorithm. Actually we don't have any vCard file
// available from real world yet, so we may need to re-enable some of these
// in the future.
Log.w(LOG_TAG, "Invalid flag is used in vCard 4.0 construction. Ignored.");
}
if (contentValuesList == null || contentValuesList.isEmpty()) {
appendLine(VCardConstants.PROPERTY_FN, "");
return this;
}
// We have difficulty here. How can we appropriately handle StructuredName with
// missing parts necessary for displaying while it has suppremental information.
//
// e.g. How to handle non-empty phonetic names with empty structured names?
final ContentValues contentValues =
getPrimaryContentValueWithStructuredName(contentValuesList);
String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
final String prefix = contentValues.getAsString(StructuredName.PREFIX);
final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
final String formattedName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
if (TextUtils.isEmpty(familyName)
&& TextUtils.isEmpty(givenName)
&& TextUtils.isEmpty(middleName)
&& TextUtils.isEmpty(prefix)
&& TextUtils.isEmpty(suffix)) {
if (TextUtils.isEmpty(formattedName)) {
appendLine(VCardConstants.PROPERTY_FN, "");
return this;
}
familyName = formattedName;
}
final String phoneticFamilyName =
contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
final String phoneticMiddleName =
contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
final String phoneticGivenName =
contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
final String escapedFamily = escapeCharacters(familyName);
final String escapedGiven = escapeCharacters(givenName);
final String escapedMiddle = escapeCharacters(middleName);
final String escapedPrefix = escapeCharacters(prefix);
final String escapedSuffix = escapeCharacters(suffix);
mBuilder.append(VCardConstants.PROPERTY_N);
if (!(TextUtils.isEmpty(phoneticFamilyName) &&
TextUtils.isEmpty(phoneticMiddleName) &&
TextUtils.isEmpty(phoneticGivenName))) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
final String sortAs = escapeCharacters(phoneticFamilyName)
+ ';' + escapeCharacters(phoneticGivenName)
+ ';' + escapeCharacters(phoneticMiddleName);
mBuilder.append("SORT-AS=").append(
VCardUtils.toStringAsV40ParamValue(sortAs));
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(escapedFamily);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(escapedGiven);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(escapedMiddle);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(escapedPrefix);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(escapedSuffix);
mBuilder.append(VCARD_END_OF_LINE);
if (TextUtils.isEmpty(formattedName)) {
// Note:
// DISPLAY_NAME doesn't exist while some other elements do, which is usually
// weird in Android, as DISPLAY_NAME should (usually) be constructed
// from the others using locale information and its code points.
Log.w(LOG_TAG, "DISPLAY_NAME is empty.");
final String escaped = escapeCharacters(VCardUtils.constructNameFromElements(
VCardConfig.getNameOrderType(mVCardType),
familyName, middleName, givenName, prefix, suffix));
appendLine(VCardConstants.PROPERTY_FN, escaped);
} else {
final String escapedFormatted = escapeCharacters(formattedName);
mBuilder.append(VCardConstants.PROPERTY_FN);
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(escapedFormatted);
mBuilder.append(VCARD_END_OF_LINE);
}
// We may need X- properties for phonetic names.
appendPhoneticNameFields(contentValues);
return this;
}
/**
* For safety, we'll emit just one value around StructuredName, as external importers
* may get confused with multiple "N", "FN", etc. properties, though it is valid in
* vCard spec.
*/
public VCardBuilder appendNameProperties(final List<ContentValues> contentValuesList) {
if (VCardConfig.isVersion40(mVCardType)) {
return appendNamePropertiesV40(contentValuesList);
}
if (contentValuesList == null || contentValuesList.isEmpty()) {
if (VCardConfig.isVersion30(mVCardType)) {
// vCard 3.0 requires "N" and "FN" properties.
// vCard 4.0 does NOT require N, but we take care of possible backward
// compatibility issues.
appendLine(VCardConstants.PROPERTY_N, "");
appendLine(VCardConstants.PROPERTY_FN, "");
} else if (mIsDoCoMo) {
appendLine(VCardConstants.PROPERTY_N, "");
}
return this;
}
final ContentValues contentValues =
getPrimaryContentValueWithStructuredName(contentValuesList);
final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
final String prefix = contentValues.getAsString(StructuredName.PREFIX);
final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) {
final boolean reallyAppendCharsetParameterToName =
shouldAppendCharsetParam(familyName, givenName, middleName, prefix, suffix);
final boolean reallyUseQuotedPrintableToName =
(!mRefrainsQPToNameProperties &&
!(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) &&
VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) &&
VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) &&
VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) &&
VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix)));
final String formattedName;
if (!TextUtils.isEmpty(displayName)) {
formattedName = displayName;
} else {
formattedName = VCardUtils.constructNameFromElements(
VCardConfig.getNameOrderType(mVCardType),
familyName, middleName, givenName, prefix, suffix);
}
final boolean reallyAppendCharsetParameterToFN =
shouldAppendCharsetParam(formattedName);
final boolean reallyUseQuotedPrintableToFN =
!mRefrainsQPToNameProperties &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName);
final String encodedFamily;
final String encodedGiven;
final String encodedMiddle;
final String encodedPrefix;
final String encodedSuffix;
if (reallyUseQuotedPrintableToName) {
encodedFamily = encodeQuotedPrintable(familyName);
encodedGiven = encodeQuotedPrintable(givenName);
encodedMiddle = encodeQuotedPrintable(middleName);
encodedPrefix = encodeQuotedPrintable(prefix);
encodedSuffix = encodeQuotedPrintable(suffix);
} else {
encodedFamily = escapeCharacters(familyName);
encodedGiven = escapeCharacters(givenName);
encodedMiddle = escapeCharacters(middleName);
encodedPrefix = escapeCharacters(prefix);
encodedSuffix = escapeCharacters(suffix);
}
final String encodedFormattedname =
(reallyUseQuotedPrintableToFN ?
encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName));
mBuilder.append(VCardConstants.PROPERTY_N);
if (mIsDoCoMo) {
if (reallyAppendCharsetParameterToName) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
if (reallyUseQuotedPrintableToName) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
// DoCoMo phones require that all the elements in the "family name" field.
mBuilder.append(formattedName);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_ITEM_SEPARATOR);
} else {
if (reallyAppendCharsetParameterToName) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
if (reallyUseQuotedPrintableToName) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(encodedFamily);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(encodedGiven);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(encodedMiddle);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(encodedPrefix);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(encodedSuffix);
}
mBuilder.append(VCARD_END_OF_LINE);
// FN property
mBuilder.append(VCardConstants.PROPERTY_FN);
if (reallyAppendCharsetParameterToFN) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
if (reallyUseQuotedPrintableToFN) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(encodedFormattedname);
mBuilder.append(VCARD_END_OF_LINE);
} else if (!TextUtils.isEmpty(displayName)) {
// N
buildSinglePartNameField(VCardConstants.PROPERTY_N, displayName);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_END_OF_LINE);
// FN
buildSinglePartNameField(VCardConstants.PROPERTY_FN, displayName);
mBuilder.append(VCARD_END_OF_LINE);
} else if (VCardConfig.isVersion30(mVCardType)) {
appendLine(VCardConstants.PROPERTY_N, "");
appendLine(VCardConstants.PROPERTY_FN, "");
} else if (mIsDoCoMo) {
appendLine(VCardConstants.PROPERTY_N, "");
}
appendPhoneticNameFields(contentValues);
return this;
}
private void buildSinglePartNameField(String property, String part) {
final boolean reallyUseQuotedPrintable =
(!mRefrainsQPToNameProperties &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(part));
final String encodedPart = reallyUseQuotedPrintable ?
encodeQuotedPrintable(part) :
escapeCharacters(part);
mBuilder.append(property);
// Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it
// when it would be useful or necessary for external importers,
// assuming the external importer allows this vioration of the spec.
if (shouldAppendCharsetParam(part)) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
if (reallyUseQuotedPrintable) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(encodedPart);
}
/**
* Emits SOUND;IRMC, SORT-STRING, and de-fact values for phonetic names like X-PHONETIC-FAMILY.
*/
private void appendPhoneticNameFields(final ContentValues contentValues) {
final String phoneticFamilyName;
final String phoneticMiddleName;
final String phoneticGivenName;
{
final String tmpPhoneticFamilyName =
contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
final String tmpPhoneticMiddleName =
contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
final String tmpPhoneticGivenName =
contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
if (mNeedsToConvertPhoneticString) {
phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName);
phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName);
phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName);
} else {
phoneticFamilyName = tmpPhoneticFamilyName;
phoneticMiddleName = tmpPhoneticMiddleName;
phoneticGivenName = tmpPhoneticGivenName;
}
}
if (TextUtils.isEmpty(phoneticFamilyName)
&& TextUtils.isEmpty(phoneticMiddleName)
&& TextUtils.isEmpty(phoneticGivenName)) {
if (mIsDoCoMo) {
mBuilder.append(VCardConstants.PROPERTY_SOUND);
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(VCARD_END_OF_LINE);
}
return;
}
if (VCardConfig.isVersion40(mVCardType)) {
// We don't want SORT-STRING anyway.
} else if (VCardConfig.isVersion30(mVCardType)) {
final String sortString =
VCardUtils.constructNameFromElements(mVCardType,
phoneticFamilyName, phoneticMiddleName, phoneticGivenName);
mBuilder.append(VCardConstants.PROPERTY_SORT_STRING);
if (VCardConfig.isVersion30(mVCardType) && shouldAppendCharsetParam(sortString)) {
// vCard 3.0 does not force us to use UTF-8 and actually we see some
// programs which emit this value. It is incorrect from the view of
// specification, but actually necessary for parsing vCard with non-UTF-8
// charsets, expecting other parsers not get confused with this value.
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(escapeCharacters(sortString));
mBuilder.append(VCARD_END_OF_LINE);
} else if (mIsJapaneseMobilePhone) {
// Note: There is no appropriate property for expressing
// phonetic name (Yomigana in Japanese) in vCard 2.1, while there is in
// vCard 3.0 (SORT-STRING).
// We use DoCoMo's way when the device is Japanese one since it is already
// supported by a lot of Japanese mobile phones.
// This is "X-" property, so any parser hopefully would not get
// confused with this.
//
// Also, DoCoMo's specification requires vCard composer to use just the first
// column.
// i.e.
// good: SOUND;X-IRMC-N:Miyakawa Daisuke;;;;
// bad : SOUND;X-IRMC-N:Miyakawa;Daisuke;;;
mBuilder.append(VCardConstants.PROPERTY_SOUND);
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
boolean reallyUseQuotedPrintable =
(!mRefrainsQPToNameProperties
&& !(VCardUtils.containsOnlyNonCrLfPrintableAscii(
phoneticFamilyName)
&& VCardUtils.containsOnlyNonCrLfPrintableAscii(
phoneticMiddleName)
&& VCardUtils.containsOnlyNonCrLfPrintableAscii(
phoneticGivenName)));
final String encodedPhoneticFamilyName;
final String encodedPhoneticMiddleName;
final String encodedPhoneticGivenName;
if (reallyUseQuotedPrintable) {
encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
} else {
encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
}
if (shouldAppendCharsetParam(encodedPhoneticFamilyName,
encodedPhoneticMiddleName, encodedPhoneticGivenName)) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
{
boolean first = true;
if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) {
mBuilder.append(encodedPhoneticFamilyName);
first = false;
}
if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) {
if (first) {
first = false;
} else {
mBuilder.append(' ');
}
mBuilder.append(encodedPhoneticMiddleName);
}
if (!TextUtils.isEmpty(encodedPhoneticGivenName)) {
if (!first) {
mBuilder.append(' ');
}
mBuilder.append(encodedPhoneticGivenName);
}
}
mBuilder.append(VCARD_ITEM_SEPARATOR); // family;given
mBuilder.append(VCARD_ITEM_SEPARATOR); // given;middle
mBuilder.append(VCARD_ITEM_SEPARATOR); // middle;prefix
mBuilder.append(VCARD_ITEM_SEPARATOR); // prefix;suffix
mBuilder.append(VCARD_END_OF_LINE);
}
if (mUsesDefactProperty) {
if (!TextUtils.isEmpty(phoneticGivenName)) {
final boolean reallyUseQuotedPrintable =
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName));
final String encodedPhoneticGivenName;
if (reallyUseQuotedPrintable) {
encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
} else {
encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
}
mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME);
if (shouldAppendCharsetParam(phoneticGivenName)) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
if (reallyUseQuotedPrintable) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(encodedPhoneticGivenName);
mBuilder.append(VCARD_END_OF_LINE);
} // if (!TextUtils.isEmpty(phoneticGivenName))
if (!TextUtils.isEmpty(phoneticMiddleName)) {
final boolean reallyUseQuotedPrintable =
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName));
final String encodedPhoneticMiddleName;
if (reallyUseQuotedPrintable) {
encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
} else {
encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
}
mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME);
if (shouldAppendCharsetParam(phoneticMiddleName)) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
if (reallyUseQuotedPrintable) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(encodedPhoneticMiddleName);
mBuilder.append(VCARD_END_OF_LINE);
} // if (!TextUtils.isEmpty(phoneticGivenName))
if (!TextUtils.isEmpty(phoneticFamilyName)) {
final boolean reallyUseQuotedPrintable =
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName));
final String encodedPhoneticFamilyName;
if (reallyUseQuotedPrintable) {
encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
} else {
encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
}
mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME);
if (shouldAppendCharsetParam(phoneticFamilyName)) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
if (reallyUseQuotedPrintable) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(encodedPhoneticFamilyName);
mBuilder.append(VCARD_END_OF_LINE);
} // if (!TextUtils.isEmpty(phoneticFamilyName))
}
}
public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) {
final boolean useAndroidProperty;
if (mIsV30OrV40) { // These specifications have NICKNAME property.
useAndroidProperty = false;
} else if (mUsesAndroidProperty) {
useAndroidProperty = true;
} else {
// There's no way to add this field.
return this;
}
if (contentValuesList != null) {
for (ContentValues contentValues : contentValuesList) {
final String nickname = contentValues.getAsString(Nickname.NAME);
if (TextUtils.isEmpty(nickname)) {
continue;
}
if (useAndroidProperty) {
appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues);
} else {
appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname);
}
}
}
return this;
}
public VCardBuilder appendPhones(final List<ContentValues> contentValuesList,
VCardPhoneNumberTranslationCallback translationCallback) {
boolean phoneLineExists = false;
if (contentValuesList != null) {
Set<String> phoneSet = new HashSet<String>();
for (ContentValues contentValues : contentValuesList) {
final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE);
final String label = contentValues.getAsString(Phone.LABEL);
final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY);
final boolean isPrimary = (isPrimaryAsInteger != null ?
(isPrimaryAsInteger > 0) : false);
String phoneNumber = contentValues.getAsString(Phone.NUMBER);
if (phoneNumber != null) {
phoneNumber = phoneNumber.trim();
}
if (TextUtils.isEmpty(phoneNumber)) {
continue;
}
final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE);
// Note: We prioritize this callback over FLAG_REFRAIN_PHONE_NUMBER_FORMATTING
// intentionally. In the future the flag will be replaced by callback
// mechanism entirely.
if (translationCallback != null) {
phoneNumber = translationCallback.onValueReceived(
phoneNumber, type, label, isPrimary);
if (!phoneSet.contains(phoneNumber)) {
phoneSet.add(phoneNumber);
appendTelLine(type, label, phoneNumber, isPrimary);
}
} else if (type == Phone.TYPE_PAGER ||
VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
// Note: PAGER number needs unformatted "phone number".
phoneLineExists = true;
if (!phoneSet.contains(phoneNumber)) {
phoneSet.add(phoneNumber);
appendTelLine(type, label, phoneNumber, isPrimary);
}
} else {
final List<String> phoneNumberList = splitPhoneNumbers(phoneNumber);
if (phoneNumberList.isEmpty()) {
continue;
}
phoneLineExists = true;
for (String actualPhoneNumber : phoneNumberList) {
if (!phoneSet.contains(actualPhoneNumber)) {
// 'p' and 'w' are the standard characters for pause and wait
// (see RFC 3601)
// so use those when exporting phone numbers via vCard.
String numberWithControlSequence = actualPhoneNumber
.replace(PhoneNumberUtils.PAUSE, 'p')
.replace(PhoneNumberUtils.WAIT, 'w');
String formatted;
// TODO: remove this code and relevant test cases. vCard and any other
// codes using it shouldn't rely on the formatter here.
if (TextUtils.equals(numberWithControlSequence, actualPhoneNumber)) {
StringBuilder digitsOnlyBuilder = new StringBuilder();
final int length = actualPhoneNumber.length();
for (int i = 0; i < length; i++) {
final char ch = actualPhoneNumber.charAt(i);
if (Character.isDigit(ch) || ch == '+') {
digitsOnlyBuilder.append(ch);
}
}
final int phoneFormat =
VCardUtils.getPhoneNumberFormat(mVCardType);
formatted = PhoneNumberUtilsPort.formatNumber(
digitsOnlyBuilder.toString(), phoneFormat);
} else {
// Be conservative.
formatted = numberWithControlSequence;
}
// In vCard 4.0, value type must be "a single URI value",
// not just a phone number. (Based on vCard 4.0 rev.13)
if (VCardConfig.isVersion40(mVCardType)
&& !TextUtils.isEmpty(formatted)
&& !formatted.startsWith("tel:")) {
formatted = "tel:" + formatted;
}
// Pre-formatted string should be stored.
phoneSet.add(actualPhoneNumber);
appendTelLine(type, label, formatted, isPrimary);
}
} // for (String actualPhoneNumber : phoneNumberList) {
// TODO: TEL with SIP URI?
}
}
}
if (!phoneLineExists && mIsDoCoMo) {
appendTelLine(Phone.TYPE_HOME, "", "", false);
}
return this;
}
/**
* <p>
* Splits a given string expressing phone numbers into several strings, and remove
* unnecessary characters inside them. The size of a returned list becomes 1 when
* no split is needed.
* </p>
* <p>
* The given number "may" have several phone numbers when the contact entry is corrupted
* because of its original source.
* e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)"
* </p>
* <p>
* This kind of "phone numbers" will not be created with Android vCard implementation,
* but we may encounter them if the source of the input data has already corrupted
* implementation.
* </p>
* <p>
* To handle this case, this method first splits its input into multiple parts
* (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and
* removes unnecessary strings like "(Miami)".
* </p>
* <p>
* Do not call this method when trimming is inappropriate for its receivers.
* </p>
*/
private List<String> splitPhoneNumbers(final String phoneNumber) {
final List<String> phoneList = new ArrayList<String>();
StringBuilder builder = new StringBuilder();
final int length = phoneNumber.length();
for (int i = 0; i < length; i++) {
final char ch = phoneNumber.charAt(i);
if (ch == '\n' && builder.length() > 0) {
phoneList.add(builder.toString());
builder = new StringBuilder();
} else {
builder.append(ch);
}
}
if (builder.length() > 0) {
phoneList.add(builder.toString());
}
return phoneList;
}
public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) {
boolean emailAddressExists = false;
if (contentValuesList != null) {
final Set<String> addressSet = new HashSet<String>();
for (ContentValues contentValues : contentValuesList) {
String emailAddress = contentValues.getAsString(Email.DATA);
if (emailAddress != null) {
emailAddress = emailAddress.trim();
}
if (TextUtils.isEmpty(emailAddress)) {
continue;
}
Integer typeAsObject = contentValues.getAsInteger(Email.TYPE);
final int type = (typeAsObject != null ?
typeAsObject : DEFAULT_EMAIL_TYPE);
final String label = contentValues.getAsString(Email.LABEL);
Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY);
final boolean isPrimary = (isPrimaryAsInteger != null ?
(isPrimaryAsInteger > 0) : false);
emailAddressExists = true;
if (!addressSet.contains(emailAddress)) {
addressSet.add(emailAddress);
appendEmailLine(type, label, emailAddress, isPrimary);
}
}
}
if (!emailAddressExists && mIsDoCoMo) {
appendEmailLine(Email.TYPE_HOME, "", "", false);
}
return this;
}
public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) {
if (contentValuesList == null || contentValuesList.isEmpty()) {
if (mIsDoCoMo) {
mBuilder.append(VCardConstants.PROPERTY_ADR);
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCardConstants.PARAM_TYPE_HOME);
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(VCARD_END_OF_LINE);
}
} else {
if (mIsDoCoMo) {
appendPostalsForDoCoMo(contentValuesList);
} else {
appendPostalsForGeneric(contentValuesList);
}
}
return this;
}
private static final Map<Integer, Integer> sPostalTypePriorityMap;
static {
sPostalTypePriorityMap = new HashMap<Integer, Integer>();
sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0);
sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1);
sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2);
sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3);
}
/**
* Tries to append just one line. If there's no appropriate address
* information, append an empty line.
*/
private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) {
int currentPriority = Integer.MAX_VALUE;
int currentType = Integer.MAX_VALUE;
ContentValues currentContentValues = null;
for (final ContentValues contentValues : contentValuesList) {
if (contentValues == null) {
continue;
}
final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger);
final int priority =
(priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE);
if (priority < currentPriority) {
currentPriority = priority;
currentType = typeAsInteger;
currentContentValues = contentValues;
if (priority == 0) {
break;
}
}
}
if (currentContentValues == null) {
Log.w(LOG_TAG, "Should not come here. Must have at least one postal data.");
return;
}
final String label = currentContentValues.getAsString(StructuredPostal.LABEL);
appendPostalLine(currentType, label, currentContentValues, false, true);
}
private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) {
for (final ContentValues contentValues : contentValuesList) {
if (contentValues == null) {
continue;
}
final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
final int type = (typeAsInteger != null ?
typeAsInteger : DEFAULT_POSTAL_TYPE);
final String label = contentValues.getAsString(StructuredPostal.LABEL);
final Integer isPrimaryAsInteger =
contentValues.getAsInteger(StructuredPostal.IS_PRIMARY);
final boolean isPrimary = (isPrimaryAsInteger != null ?
(isPrimaryAsInteger > 0) : false);
appendPostalLine(type, label, contentValues, isPrimary, false);
}
}
private static class PostalStruct {
final boolean reallyUseQuotedPrintable;
final boolean appendCharset;
final String addressData;
public PostalStruct(final boolean reallyUseQuotedPrintable,
final boolean appendCharset, final String addressData) {
this.reallyUseQuotedPrintable = reallyUseQuotedPrintable;
this.appendCharset = appendCharset;
this.addressData = addressData;
}
}
/**
* @return null when there's no information available to construct the data.
*/
private PostalStruct tryConstructPostalStruct(ContentValues contentValues) {
// adr-value = 0*6(text-value ";") text-value
// ; PO Box, Extended Address, Street, Locality, Region, Postal
// ; Code, Country Name
final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX);
final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD);
final String rawStreet = contentValues.getAsString(StructuredPostal.STREET);
final String rawLocality = contentValues.getAsString(StructuredPostal.CITY);
final String rawRegion = contentValues.getAsString(StructuredPostal.REGION);
final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE);
final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY);
final String[] rawAddressArray = new String[]{
rawPoBox, rawNeighborhood, rawStreet, rawLocality,
rawRegion, rawPostalCode, rawCountry};
if (!VCardUtils.areAllEmpty(rawAddressArray)) {
final boolean reallyUseQuotedPrintable =
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray));
final boolean appendCharset =
!VCardUtils.containsOnlyPrintableAscii(rawAddressArray);
final String encodedPoBox;
final String encodedStreet;
final String encodedLocality;
final String encodedRegion;
final String encodedPostalCode;
final String encodedCountry;
final String encodedNeighborhood;
final String rawLocality2;
// This looks inefficient since we encode rawLocality and rawNeighborhood twice,
// but this is intentional.
//
// QP encoding may add line feeds when needed and the result of
// - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood)
// may be different from
// - encodedLocality + " " + encodedNeighborhood.
//
// We use safer way.
if (TextUtils.isEmpty(rawLocality)) {
if (TextUtils.isEmpty(rawNeighborhood)) {
rawLocality2 = "";
} else {
rawLocality2 = rawNeighborhood;
}
} else {
if (TextUtils.isEmpty(rawNeighborhood)) {
rawLocality2 = rawLocality;
} else {
rawLocality2 = rawLocality + " " + rawNeighborhood;
}
}
if (reallyUseQuotedPrintable) {
encodedPoBox = encodeQuotedPrintable(rawPoBox);
encodedStreet = encodeQuotedPrintable(rawStreet);
encodedLocality = encodeQuotedPrintable(rawLocality2);
encodedRegion = encodeQuotedPrintable(rawRegion);
encodedPostalCode = encodeQuotedPrintable(rawPostalCode);
encodedCountry = encodeQuotedPrintable(rawCountry);
} else {
encodedPoBox = escapeCharacters(rawPoBox);
encodedStreet = escapeCharacters(rawStreet);
encodedLocality = escapeCharacters(rawLocality2);
encodedRegion = escapeCharacters(rawRegion);
encodedPostalCode = escapeCharacters(rawPostalCode);
encodedCountry = escapeCharacters(rawCountry);
encodedNeighborhood = escapeCharacters(rawNeighborhood);
}
final StringBuilder addressBuilder = new StringBuilder();
addressBuilder.append(encodedPoBox);
addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address
addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street
addressBuilder.append(encodedStreet);
addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality
addressBuilder.append(encodedLocality);
addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region
addressBuilder.append(encodedRegion);
addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code
addressBuilder.append(encodedPostalCode);
addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country
addressBuilder.append(encodedCountry);
return new PostalStruct(
reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
} else { // VCardUtils.areAllEmpty(rawAddressArray) == true
// Try to use FORMATTED_ADDRESS instead.
final String rawFormattedAddress =
contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
if (TextUtils.isEmpty(rawFormattedAddress)) {
return null;
}
final boolean reallyUseQuotedPrintable =
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress));
final boolean appendCharset =
!VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress);
final String encodedFormattedAddress;
if (reallyUseQuotedPrintable) {
encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress);
} else {
encodedFormattedAddress = escapeCharacters(rawFormattedAddress);
}
// We use the second value ("Extended Address") just because Japanese mobile phones
// do so. If the other importer expects the value be in the other field, some flag may
// be needed.
final StringBuilder addressBuilder = new StringBuilder();
addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address
addressBuilder.append(encodedFormattedAddress);
addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street
addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality
addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region
addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code
addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country
return new PostalStruct(
reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
}
}
public VCardBuilder appendIms(final List<ContentValues> contentValuesList) {
if (contentValuesList != null) {
for (ContentValues contentValues : contentValuesList) {
final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL);
if (protocolAsObject == null) {
continue;
}
final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject);
if (propertyName == null) {
continue;
}
String data = contentValues.getAsString(Im.DATA);
if (data != null) {
data = data.trim();
}
if (TextUtils.isEmpty(data)) {
continue;
}
final String typeAsString;
{
final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE);
switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) {
case Im.TYPE_HOME: {
typeAsString = VCardConstants.PARAM_TYPE_HOME;
break;
}
case Im.TYPE_WORK: {
typeAsString = VCardConstants.PARAM_TYPE_WORK;
break;
}
case Im.TYPE_CUSTOM: {
final String label = contentValues.getAsString(Im.LABEL);
typeAsString = (label != null ? "X-" + label : null);
break;
}
case Im.TYPE_OTHER: // Ignore
default: {
typeAsString = null;
break;
}
}
}
final List<String> parameterList = new ArrayList<String>();
if (!TextUtils.isEmpty(typeAsString)) {
parameterList.add(typeAsString);
}
final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY);
final boolean isPrimary = (isPrimaryAsInteger != null ?
(isPrimaryAsInteger > 0) : false);
if (isPrimary) {
parameterList.add(VCardConstants.PARAM_TYPE_PREF);
}
appendLineWithCharsetAndQPDetection(propertyName, parameterList, data);
}
}
return this;
}
public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) {
if (contentValuesList != null) {
for (ContentValues contentValues : contentValuesList) {
String website = contentValues.getAsString(Website.URL);
if (website != null) {
website = website.trim();
}
// Note: vCard 3.0 does not allow any parameter addition toward "URL"
// property, while there's no document in vCard 2.1.
if (!TextUtils.isEmpty(website)) {
appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website);
}
}
}
return this;
}
public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) {
if (contentValuesList != null) {
for (ContentValues contentValues : contentValuesList) {
String company = contentValues.getAsString(Organization.COMPANY);
if (company != null) {
company = company.trim();
}
String department = contentValues.getAsString(Organization.DEPARTMENT);
if (department != null) {
department = department.trim();
}
String title = contentValues.getAsString(Organization.TITLE);
if (title != null) {
title = title.trim();
}
StringBuilder orgBuilder = new StringBuilder();
if (!TextUtils.isEmpty(company)) {
orgBuilder.append(company);
}
if (!TextUtils.isEmpty(department)) {
if (orgBuilder.length() > 0) {
orgBuilder.append(';');
}
orgBuilder.append(department);
}
final String orgline = orgBuilder.toString();
appendLine(VCardConstants.PROPERTY_ORG, orgline,
!VCardUtils.containsOnlyPrintableAscii(orgline),
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline)));
if (!TextUtils.isEmpty(title)) {
appendLine(VCardConstants.PROPERTY_TITLE, title,
!VCardUtils.containsOnlyPrintableAscii(title),
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(title)));
}
}
}
return this;
}
public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) {
if (contentValuesList != null) {
for (ContentValues contentValues : contentValuesList) {
if (contentValues == null) {
continue;
}
byte[] data = contentValues.getAsByteArray(Photo.PHOTO);
if (data == null) {
continue;
}
final String photoType = VCardUtils.guessImageType(data);
if (photoType == null) {
Log.d(LOG_TAG, "Unknown photo type. Ignored.");
continue;
}
// TODO: check this works fine.
final String photoString = new String(Base64.encode(data, Base64.NO_WRAP));
if (!TextUtils.isEmpty(photoString)) {
appendPhotoLine(photoString, photoType);
}
}
}
return this;
}
public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) {
if (contentValuesList != null) {
if (mOnlyOneNoteFieldIsAvailable) {
final StringBuilder noteBuilder = new StringBuilder();
boolean first = true;
for (final ContentValues contentValues : contentValuesList) {
String note = contentValues.getAsString(Note.NOTE);
if (note == null) {
note = "";
}
if (note.length() > 0) {
if (first) {
first = false;
} else {
noteBuilder.append('\n');
}
noteBuilder.append(note);
}
}
final String noteStr = noteBuilder.toString();
// This means we scan noteStr completely twice, which is redundant.
// But for now, we assume this is not so time-consuming..
final boolean shouldAppendCharsetInfo =
!VCardUtils.containsOnlyPrintableAscii(noteStr);
final boolean reallyUseQuotedPrintable =
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
shouldAppendCharsetInfo, reallyUseQuotedPrintable);
} else {
for (ContentValues contentValues : contentValuesList) {
final String noteStr = contentValues.getAsString(Note.NOTE);
if (!TextUtils.isEmpty(noteStr)) {
final boolean shouldAppendCharsetInfo =
!VCardUtils.containsOnlyPrintableAscii(noteStr);
final boolean reallyUseQuotedPrintable =
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
shouldAppendCharsetInfo, reallyUseQuotedPrintable);
}
}
}
}
return this;
}
public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) {
// There's possibility where a given object may have more than one birthday, which
// is inappropriate. We just build one birthday.
if (contentValuesList != null) {
String primaryBirthday = null;
String secondaryBirthday = null;
for (final ContentValues contentValues : contentValuesList) {
if (contentValues == null) {
continue;
}
final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE);
final int eventType;
if (eventTypeAsInteger != null) {
eventType = eventTypeAsInteger;
} else {
eventType = Event.TYPE_OTHER;
}
if (eventType == Event.TYPE_BIRTHDAY) {
final String birthdayCandidate = contentValues.getAsString(Event.START_DATE);
if (birthdayCandidate == null) {
continue;
}
final Integer isSuperPrimaryAsInteger =
contentValues.getAsInteger(Event.IS_SUPER_PRIMARY);
final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ?
(isSuperPrimaryAsInteger > 0) : false);
if (isSuperPrimary) {
// "super primary" birthday should the prefered one.
primaryBirthday = birthdayCandidate;
break;
}
final Integer isPrimaryAsInteger =
contentValues.getAsInteger(Event.IS_PRIMARY);
final boolean isPrimary = (isPrimaryAsInteger != null ?
(isPrimaryAsInteger > 0) : false);
if (isPrimary) {
// We don't break here since "super primary" birthday may exist later.
primaryBirthday = birthdayCandidate;
} else if (secondaryBirthday == null) {
// First entry is set to the "secondary" candidate.
secondaryBirthday = birthdayCandidate;
}
} else if (mUsesAndroidProperty) {
// Event types other than Birthday is not supported by vCard.
appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues);
}
}
if (primaryBirthday != null) {
appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
primaryBirthday.trim());
} else if (secondaryBirthday != null){
appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
secondaryBirthday.trim());
}
}
return this;
}
public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) {
if (mUsesAndroidProperty && contentValuesList != null) {
for (final ContentValues contentValues : contentValuesList) {
if (contentValues == null) {
continue;
}
appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues);
}
}
return this;
}
/**
* @param emitEveryTime If true, builder builds the line even when there's no entry.
*/
public void appendPostalLine(final int type, final String label,
final ContentValues contentValues,
final boolean isPrimary, final boolean emitEveryTime) {
final boolean reallyUseQuotedPrintable;
final boolean appendCharset;
final String addressValue;
{
PostalStruct postalStruct = tryConstructPostalStruct(contentValues);
if (postalStruct == null) {
if (emitEveryTime) {
reallyUseQuotedPrintable = false;
appendCharset = false;
addressValue = "";
} else {
return;
}
} else {
reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable;
appendCharset = postalStruct.appendCharset;
addressValue = postalStruct.addressData;
}
}
List<String> parameterList = new ArrayList<String>();
if (isPrimary) {
parameterList.add(VCardConstants.PARAM_TYPE_PREF);
}
switch (type) {
case StructuredPostal.TYPE_HOME: {
parameterList.add(VCardConstants.PARAM_TYPE_HOME);
break;
}
case StructuredPostal.TYPE_WORK: {
parameterList.add(VCardConstants.PARAM_TYPE_WORK);
break;
}
case StructuredPostal.TYPE_CUSTOM: {
if (!TextUtils.isEmpty(label)
&& VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
// We're not sure whether the label is valid in the spec
// ("IANA-token" in the vCard 3.0 is unclear...)
// Just for safety, we add "X-" at the beggining of each label.
// Also checks the label obeys with vCard 3.0 spec.
parameterList.add("X-" + label);
}
break;
}
case StructuredPostal.TYPE_OTHER: {
break;
}
default: {
Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type);
break;
}
}
mBuilder.append(VCardConstants.PROPERTY_ADR);
if (!parameterList.isEmpty()) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
appendTypeParameters(parameterList);
}
if (appendCharset) {
// Strictly, vCard 3.0 does not allow exporters to emit charset information,
// but we will add it since the information should be useful for importers,
//
// Assume no parser does not emit error with this parameter in vCard 3.0.
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
if (reallyUseQuotedPrintable) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(addressValue);
mBuilder.append(VCARD_END_OF_LINE);
}
public void appendEmailLine(final int type, final String label,
final String rawValue, final boolean isPrimary) {
final String typeAsString;
switch (type) {
case Email.TYPE_CUSTOM: {
if (VCardUtils.isMobilePhoneLabel(label)) {
typeAsString = VCardConstants.PARAM_TYPE_CELL;
} else if (!TextUtils.isEmpty(label)
&& VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
typeAsString = "X-" + label;
} else {
typeAsString = null;
}
break;
}
case Email.TYPE_HOME: {
typeAsString = VCardConstants.PARAM_TYPE_HOME;
break;
}
case Email.TYPE_WORK: {
typeAsString = VCardConstants.PARAM_TYPE_WORK;
break;
}
case Email.TYPE_OTHER: {
typeAsString = null;
break;
}
case Email.TYPE_MOBILE: {
typeAsString = VCardConstants.PARAM_TYPE_CELL;
break;
}
default: {
Log.e(LOG_TAG, "Unknown Email type: " + type);
typeAsString = null;
break;
}
}
final List<String> parameterList = new ArrayList<String>();
if (isPrimary) {
parameterList.add(VCardConstants.PARAM_TYPE_PREF);
}
if (!TextUtils.isEmpty(typeAsString)) {
parameterList.add(typeAsString);
}
appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList,
rawValue);
}
public void appendTelLine(final Integer typeAsInteger, final String label,
final String encodedValue, boolean isPrimary) {
mBuilder.append(VCardConstants.PROPERTY_TEL);
mBuilder.append(VCARD_PARAM_SEPARATOR);
final int type;
if (typeAsInteger == null) {
type = Phone.TYPE_OTHER;
} else {
type = typeAsInteger;
}
ArrayList<String> parameterList = new ArrayList<String>();
switch (type) {
case Phone.TYPE_HOME: {
parameterList.addAll(
Arrays.asList(VCardConstants.PARAM_TYPE_HOME));
break;
}
case Phone.TYPE_WORK: {
parameterList.addAll(
Arrays.asList(VCardConstants.PARAM_TYPE_WORK));
break;
}
case Phone.TYPE_FAX_HOME: {
parameterList.addAll(
Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX));
break;
}
case Phone.TYPE_FAX_WORK: {
parameterList.addAll(
Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX));
break;
}
case Phone.TYPE_MOBILE: {
parameterList.add(VCardConstants.PARAM_TYPE_CELL);
break;
}
case Phone.TYPE_PAGER: {
if (mIsDoCoMo) {
// Not sure about the reason, but previous implementation had
// used "VOICE" instead of "PAGER"
parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
} else {
parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
}
break;
}
case Phone.TYPE_OTHER: {
parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
break;
}
case Phone.TYPE_CAR: {
parameterList.add(VCardConstants.PARAM_TYPE_CAR);
break;
}
case Phone.TYPE_COMPANY_MAIN: {
// There's no relevant field in vCard (at least 2.1).
parameterList.add(VCardConstants.PARAM_TYPE_WORK);
isPrimary = true;
break;
}
case Phone.TYPE_ISDN: {
parameterList.add(VCardConstants.PARAM_TYPE_ISDN);
break;
}
case Phone.TYPE_MAIN: {
isPrimary = true;
break;
}
case Phone.TYPE_OTHER_FAX: {
parameterList.add(VCardConstants.PARAM_TYPE_FAX);
break;
}
case Phone.TYPE_TELEX: {
parameterList.add(VCardConstants.PARAM_TYPE_TLX);
break;
}
case Phone.TYPE_WORK_MOBILE: {
parameterList.addAll(
Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL));
break;
}
case Phone.TYPE_WORK_PAGER: {
parameterList.add(VCardConstants.PARAM_TYPE_WORK);
// See above.
if (mIsDoCoMo) {
parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
} else {
parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
}
break;
}
case Phone.TYPE_MMS: {
parameterList.add(VCardConstants.PARAM_TYPE_MSG);
break;
}
case Phone.TYPE_CUSTOM: {
if (TextUtils.isEmpty(label)) {
// Just ignore the custom type.
parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
} else if (VCardUtils.isMobilePhoneLabel(label)) {
parameterList.add(VCardConstants.PARAM_TYPE_CELL);
} else if (mIsV30OrV40) {
// This label is appropriately encoded in appendTypeParameters.
parameterList.add(label);
} else {
final String upperLabel = label.toUpperCase();
if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) {
parameterList.add(upperLabel);
} else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
// Note: Strictly, vCard 2.1 does not allow "X-" parameter without
// "TYPE=" string.
parameterList.add("X-" + label);
}
}
break;
}
case Phone.TYPE_RADIO:
case Phone.TYPE_TTY_TDD:
default: {
break;
}
}
if (isPrimary) {
parameterList.add(VCardConstants.PARAM_TYPE_PREF);
}
if (parameterList.isEmpty()) {
appendUncommonPhoneType(mBuilder, type);
} else {
appendTypeParameters(parameterList);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(encodedValue);
mBuilder.append(VCARD_END_OF_LINE);
}
/**
* Appends phone type string which may not be available in some devices.
*/
private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) {
if (mIsDoCoMo) {
// The previous implementation for DoCoMo had been conservative
// about miscellaneous types.
builder.append(VCardConstants.PARAM_TYPE_VOICE);
} else {
String phoneType = VCardUtils.getPhoneTypeString(type);
if (phoneType != null) {
appendTypeParameter(phoneType);
} else {
Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type);
}
}
}
/**
* @param encodedValue Must be encoded by BASE64
* @param photoType
*/
public void appendPhotoLine(final String encodedValue, final String photoType) {
StringBuilder tmpBuilder = new StringBuilder();
tmpBuilder.append(VCardConstants.PROPERTY_PHOTO);
tmpBuilder.append(VCARD_PARAM_SEPARATOR);
if (mIsV30OrV40) {
tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_AS_B);
} else {
tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21);
}
tmpBuilder.append(VCARD_PARAM_SEPARATOR);
appendTypeParameter(tmpBuilder, photoType);
tmpBuilder.append(VCARD_DATA_SEPARATOR);
tmpBuilder.append(encodedValue);
final String tmpStr = tmpBuilder.toString();
tmpBuilder = new StringBuilder();
int lineCount = 0;
final int length = tmpStr.length();
final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30
- VCARD_END_OF_LINE.length();
final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length();
int maxNum = maxNumForFirstLine;
for (int i = 0; i < length; i++) {
tmpBuilder.append(tmpStr.charAt(i));
lineCount++;
if (lineCount > maxNum) {
tmpBuilder.append(VCARD_END_OF_LINE);
tmpBuilder.append(VCARD_WS);
maxNum = maxNumInGeneral;
lineCount = 0;
}
}
mBuilder.append(tmpBuilder.toString());
mBuilder.append(VCARD_END_OF_LINE);
mBuilder.append(VCARD_END_OF_LINE);
}
/**
* SIP (Session Initiation Protocol) is first supported in RFC 4770 as part of IMPP
* support. vCard 2.1 and old vCard 3.0 may not able to parse it, or expect X-SIP
* instead of "IMPP;sip:...".
*
* We honor RFC 4770 and don't allow vCard 3.0 to emit X-SIP at all.
*/
public VCardBuilder appendSipAddresses(final List<ContentValues> contentValuesList) {
final boolean useXProperty;
if (mIsV30OrV40) {
useXProperty = false;
} else if (mUsesDefactProperty){
useXProperty = true;
} else {
return this;
}
if (contentValuesList != null) {
for (ContentValues contentValues : contentValuesList) {
String sipAddress = contentValues.getAsString(SipAddress.SIP_ADDRESS);
if (TextUtils.isEmpty(sipAddress)) {
continue;
}
if (useXProperty) {
// X-SIP does not contain "sip:" prefix.
if (sipAddress.startsWith("sip:")) {
if (sipAddress.length() == 4) {
continue;
}
sipAddress = sipAddress.substring(4);
}
// No type is available yet.
appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_X_SIP, sipAddress);
} else {
if (!sipAddress.startsWith("sip:")) {
sipAddress = "sip:" + sipAddress;
}
final String propertyName;
if (VCardConfig.isVersion40(mVCardType)) {
// We have two ways to emit sip address: TEL and IMPP. Currently (rev.13)
// TEL seems appropriate but may change in the future.
propertyName = VCardConstants.PROPERTY_TEL;
} else {
// RFC 4770 (for vCard 3.0)
propertyName = VCardConstants.PROPERTY_IMPP;
}
appendLineWithCharsetAndQPDetection(propertyName, sipAddress);
}
}
}
return this;
}
public void appendAndroidSpecificProperty(
final String mimeType, ContentValues contentValues) {
if (!sAllowedAndroidPropertySet.contains(mimeType)) {
return;
}
final List<String> rawValueList = new ArrayList<String>();
for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) {
String value = contentValues.getAsString("data" + i);
if (value == null) {
value = "";
}
rawValueList.add(value);
}
boolean needCharset =
(mShouldAppendCharsetParam &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
boolean reallyUseQuotedPrintable =
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM);
if (needCharset) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
if (reallyUseQuotedPrintable) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(mimeType); // Should not be encoded.
for (String rawValue : rawValueList) {
final String encodedValue;
if (reallyUseQuotedPrintable) {
encodedValue = encodeQuotedPrintable(rawValue);
} else {
// TODO: one line may be too huge, which may be invalid in vCard 3.0
// (which says "When generating a content line, lines longer than
// 75 characters SHOULD be folded"), though several
// (even well-known) applications do not care this.
encodedValue = escapeCharacters(rawValue);
}
mBuilder.append(VCARD_ITEM_SEPARATOR);
mBuilder.append(encodedValue);
}
mBuilder.append(VCARD_END_OF_LINE);
}
public void appendLineWithCharsetAndQPDetection(final String propertyName,
final String rawValue) {
appendLineWithCharsetAndQPDetection(propertyName, null, rawValue);
}
public void appendLineWithCharsetAndQPDetection(
final String propertyName, final List<String> rawValueList) {
appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList);
}
public void appendLineWithCharsetAndQPDetection(final String propertyName,
final List<String> parameterList, final String rawValue) {
final boolean needCharset =
!VCardUtils.containsOnlyPrintableAscii(rawValue);
final boolean reallyUseQuotedPrintable =
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue));
appendLine(propertyName, parameterList,
rawValue, needCharset, reallyUseQuotedPrintable);
}
public void appendLineWithCharsetAndQPDetection(final String propertyName,
final List<String> parameterList, final List<String> rawValueList) {
boolean needCharset =
(mShouldAppendCharsetParam &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
boolean reallyUseQuotedPrintable =
(mShouldUseQuotedPrintable &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
appendLine(propertyName, parameterList, rawValueList,
needCharset, reallyUseQuotedPrintable);
}
/**
* Appends one line with a given property name and value.
*/
public void appendLine(final String propertyName, final String rawValue) {
appendLine(propertyName, rawValue, false, false);
}
public void appendLine(final String propertyName, final List<String> rawValueList) {
appendLine(propertyName, rawValueList, false, false);
}
public void appendLine(final String propertyName,
final String rawValue, final boolean needCharset,
boolean reallyUseQuotedPrintable) {
appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable);
}
public void appendLine(final String propertyName, final List<String> parameterList,
final String rawValue) {
appendLine(propertyName, parameterList, rawValue, false, false);
}
public void appendLine(final String propertyName, final List<String> parameterList,
final String rawValue, final boolean needCharset,
boolean reallyUseQuotedPrintable) {
mBuilder.append(propertyName);
if (parameterList != null && parameterList.size() > 0) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
appendTypeParameters(parameterList);
}
if (needCharset) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
final String encodedValue;
if (reallyUseQuotedPrintable) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
encodedValue = encodeQuotedPrintable(rawValue);
} else {
// TODO: one line may be too huge, which may be invalid in vCard spec, though
// several (even well-known) applications do not care that violation.
encodedValue = escapeCharacters(rawValue);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
mBuilder.append(encodedValue);
mBuilder.append(VCARD_END_OF_LINE);
}
public void appendLine(final String propertyName, final List<String> rawValueList,
final boolean needCharset, boolean needQuotedPrintable) {
appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable);
}
public void appendLine(final String propertyName, final List<String> parameterList,
final List<String> rawValueList, final boolean needCharset,
final boolean needQuotedPrintable) {
mBuilder.append(propertyName);
if (parameterList != null && parameterList.size() > 0) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
appendTypeParameters(parameterList);
}
if (needCharset) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(mVCardCharsetParameter);
}
if (needQuotedPrintable) {
mBuilder.append(VCARD_PARAM_SEPARATOR);
mBuilder.append(VCARD_PARAM_ENCODING_QP);
}
mBuilder.append(VCARD_DATA_SEPARATOR);
boolean first = true;
for (String rawValue : rawValueList) {
final String encodedValue;
if (needQuotedPrintable) {
encodedValue = encodeQuotedPrintable(rawValue);
} else {
// TODO: one line may be too huge, which may be invalid in vCard 3.0
// (which says "When generating a content line, lines longer than
// 75 characters SHOULD be folded"), though several
// (even well-known) applications do not care this.
encodedValue = escapeCharacters(rawValue);
}
if (first) {
first = false;
} else {
mBuilder.append(VCARD_ITEM_SEPARATOR);
}
mBuilder.append(encodedValue);
}
mBuilder.append(VCARD_END_OF_LINE);
}
/**
* VCARD_PARAM_SEPARATOR must be appended before this method being called.
*/
private void appendTypeParameters(final List<String> types) {
// We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
// which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
boolean first = true;
for (final String typeValue : types) {
if (VCardConfig.isVersion30(mVCardType) || VCardConfig.isVersion40(mVCardType)) {
final String encoded = (VCardConfig.isVersion40(mVCardType) ?
VCardUtils.toStringAsV40ParamValue(typeValue) :
VCardUtils.toStringAsV30ParamValue(typeValue));
if (TextUtils.isEmpty(encoded)) {
continue;
}
if (first) {
first = false;
} else {
mBuilder.append(VCARD_PARAM_SEPARATOR);
}
appendTypeParameter(encoded);
} else { // vCard 2.1
if (!VCardUtils.isV21Word(typeValue)) {
continue;
}
if (first) {
first = false;
} else {
mBuilder.append(VCARD_PARAM_SEPARATOR);
}
appendTypeParameter(typeValue);
}
}
}
/**
* VCARD_PARAM_SEPARATOR must be appended before this method being called.
*/
private void appendTypeParameter(final String type) {
appendTypeParameter(mBuilder, type);
}
private void appendTypeParameter(final StringBuilder builder, final String type) {
// Refrain from using appendType() so that "TYPE=" is not be appended when the
// device is DoCoMo's (just for safety).
//
// Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
if (VCardConfig.isVersion40(mVCardType) ||
((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) {
builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
}
builder.append(type);
}
/**
* Returns true when the property line should contain charset parameter
* information. This method may return true even when vCard version is 3.0.
*
* Strictly, adding charset information is invalid in VCard 3.0.
* However we'll add the info only when charset we use is not UTF-8
* in vCard 3.0 format, since parser side may be able to use the charset
* via this field, though we may encounter another problem by adding it.
*
* e.g. Japanese mobile phones use Shift_Jis while RFC 2426
* recommends UTF-8. By adding this field, parsers may be able
* to know this text is NOT UTF-8 but Shift_Jis.
*/
private boolean shouldAppendCharsetParam(String...propertyValueList) {
if (!mShouldAppendCharsetParam) {
return false;
}
for (String propertyValue : propertyValueList) {
if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
return true;
}
}
return false;
}
private String encodeQuotedPrintable(final String str) {
if (TextUtils.isEmpty(str)) {
return "";
}
final StringBuilder builder = new StringBuilder();
int index = 0;
int lineCount = 0;
byte[] strArray = null;
try {
strArray = str.getBytes(mCharset);
} catch (UnsupportedEncodingException e) {
Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. "
+ "Try default charset");
strArray = str.getBytes();
}
while (index < strArray.length) {
builder.append(String.format("=%02X", strArray[index]));
index += 1;
lineCount += 3;
if (lineCount >= 67) {
// Specification requires CRLF must be inserted before the
// length of the line
// becomes more than 76.
// Assuming that the next character is a multi-byte character,
// it will become
// 6 bytes.
// 76 - 6 - 3 = 67
builder.append("=\r\n");
lineCount = 0;
}
}
return builder.toString();
}
/**
* Append '\' to the characters which should be escaped. The character set is different
* not only between vCard 2.1 and vCard 3.0 but also among each device.
*
* Note that Quoted-Printable string must not be input here.
*/
@SuppressWarnings("fallthrough")
private String escapeCharacters(final String unescaped) {
if (TextUtils.isEmpty(unescaped)) {
return "";
}
final StringBuilder tmpBuilder = new StringBuilder();
final int length = unescaped.length();
for (int i = 0; i < length; i++) {
final char ch = unescaped.charAt(i);
switch (ch) {
case ';': {
tmpBuilder.append('\\');
tmpBuilder.append(';');
break;
}
case '\r': {
if (i + 1 < length) {
char nextChar = unescaped.charAt(i);
if (nextChar == '\n') {
break;
} else {
// fall through
}
} else {
// fall through
}
}
case '\n': {
// In vCard 2.1, there's no specification about this, while
// vCard 3.0 explicitly requires this should be encoded to "\n".
tmpBuilder.append("\\n");
break;
}
case '\\': {
if (mIsV30OrV40) {
tmpBuilder.append("\\\\");
break;
} else {
// fall through
}
}
case '<':
case '>': {
if (mIsDoCoMo) {
tmpBuilder.append('\\');
tmpBuilder.append(ch);
} else {
tmpBuilder.append(ch);
}
break;
}
case ',': {
if (mIsV30OrV40) {
tmpBuilder.append("\\,");
} else {
tmpBuilder.append(ch);
}
break;
}
default: {
tmpBuilder.append(ch);
break;
}
}
}
return tmpBuilder.toString();
}
@Override
public String toString() {
if (!mEndAppended) {
if (mIsDoCoMo) {
appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
appendLine(VCardConstants.PROPERTY_X_REDUCTION, "");
appendLine(VCardConstants.PROPERTY_X_NO, "");
appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, "");
}
appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD);
mEndAppended = true;
}
return mBuilder.toString();
}
}