blob: 13277704bdcf82082021285acf6b6de208802495 [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 android.pim.vcard;
import android.accounts.Account;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
/**
* This class bridges between data structure of Contact app and VCard data.
*/
public class VCardEntry {
private static final String LOG_TAG = "VCardEntry";
private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK;
private static final String ACCOUNT_TYPE_GOOGLE = "com.google";
private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts";
private static final Map<String, Integer> sImMap = new HashMap<String, Integer>();
static {
sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM);
sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN);
sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO);
sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ);
sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER);
sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE);
sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK);
sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE,
Im.PROTOCOL_GOOGLE_TALK);
}
static public class PhoneData {
public final int type;
public final String data;
public final String label;
// isPrimary is changable only when there's no appropriate one existing in
// the original VCard.
public boolean isPrimary;
public PhoneData(int type, String data, String label, boolean isPrimary) {
this.type = type;
this.data = data;
this.label = label;
this.isPrimary = isPrimary;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PhoneData)) {
return false;
}
PhoneData phoneData = (PhoneData)obj;
return (type == phoneData.type && data.equals(phoneData.data) &&
label.equals(phoneData.label) && isPrimary == phoneData.isPrimary);
}
@Override
public String toString() {
return String.format("type: %d, data: %s, label: %s, isPrimary: %s",
type, data, label, isPrimary);
}
}
static public class EmailData {
public final int type;
public final String data;
// Used only when TYPE is TYPE_CUSTOM.
public final String label;
// isPrimary is changable only when there's no appropriate one existing in
// the original VCard.
public boolean isPrimary;
public EmailData(int type, String data, String label, boolean isPrimary) {
this.type = type;
this.data = data;
this.label = label;
this.isPrimary = isPrimary;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof EmailData)) {
return false;
}
EmailData emailData = (EmailData)obj;
return (type == emailData.type && data.equals(emailData.data) &&
label.equals(emailData.label) && isPrimary == emailData.isPrimary);
}
@Override
public String toString() {
return String.format("type: %d, data: %s, label: %s, isPrimary: %s",
type, data, label, isPrimary);
}
}
static public class PostalData {
// Determined by vCard spec.
// PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name
public static final int ADDR_MAX_DATA_SIZE = 7;
private final String[] dataArray;
public final String pobox;
public final String extendedAddress;
public final String street;
public final String localty;
public final String region;
public final String postalCode;
public final String country;
public final int type;
public final String label;
public boolean isPrimary;
public PostalData(final int type, final List<String> propValueList,
final String label, boolean isPrimary) {
this.type = type;
dataArray = new String[ADDR_MAX_DATA_SIZE];
int size = propValueList.size();
if (size > ADDR_MAX_DATA_SIZE) {
size = ADDR_MAX_DATA_SIZE;
}
// adr-value = 0*6(text-value ";") text-value
// ; PO Box, Extended Address, Street, Locality, Region, Postal
// ; Code, Country Name
//
// Use Iterator assuming List may be LinkedList, though actually it is
// always ArrayList in the current implementation.
int i = 0;
for (String addressElement : propValueList) {
dataArray[i] = addressElement;
if (++i >= size) {
break;
}
}
while (i < ADDR_MAX_DATA_SIZE) {
dataArray[i++] = null;
}
this.pobox = dataArray[0];
this.extendedAddress = dataArray[1];
this.street = dataArray[2];
this.localty = dataArray[3];
this.region = dataArray[4];
this.postalCode = dataArray[5];
this.country = dataArray[6];
this.label = label;
this.isPrimary = isPrimary;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PostalData)) {
return false;
}
final PostalData postalData = (PostalData)obj;
return (Arrays.equals(dataArray, postalData.dataArray) &&
(type == postalData.type &&
(type == StructuredPostal.TYPE_CUSTOM ?
(label == postalData.label) : true)) &&
(isPrimary == postalData.isPrimary));
}
public String getFormattedAddress(final int vcardType) {
StringBuilder builder = new StringBuilder();
boolean empty = true;
if (VCardConfig.isJapaneseDevice(vcardType)) {
// In Japan, the order is reversed.
for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) {
String addressPart = dataArray[i];
if (!TextUtils.isEmpty(addressPart)) {
if (!empty) {
builder.append(' ');
} else {
empty = false;
}
builder.append(addressPart);
}
}
} else {
for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) {
String addressPart = dataArray[i];
if (!TextUtils.isEmpty(addressPart)) {
if (!empty) {
builder.append(' ');
} else {
empty = false;
}
builder.append(addressPart);
}
}
}
return builder.toString().trim();
}
@Override
public String toString() {
return String.format("type: %d, label: %s, isPrimary: %s",
type, label, isPrimary);
}
}
static public class OrganizationData {
public final int type;
// non-final is Intentional: we may change the values since this info is separated into
// two parts in vCard: "ORG" + "TITLE".
public String companyName;
public String departmentName;
public String titleName;
public boolean isPrimary;
public OrganizationData(int type,
String companyName,
String departmentName,
String titleName,
boolean isPrimary) {
this.type = type;
this.companyName = companyName;
this.departmentName = departmentName;
this.titleName = titleName;
this.isPrimary = isPrimary;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof OrganizationData)) {
return false;
}
OrganizationData organization = (OrganizationData)obj;
return (type == organization.type &&
TextUtils.equals(companyName, organization.companyName) &&
TextUtils.equals(departmentName, organization.departmentName) &&
TextUtils.equals(titleName, organization.titleName) &&
isPrimary == organization.isPrimary);
}
public String getFormattedString() {
final StringBuilder builder = new StringBuilder();
if (!TextUtils.isEmpty(companyName)) {
builder.append(companyName);
}
if (!TextUtils.isEmpty(departmentName)) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(departmentName);
}
if (!TextUtils.isEmpty(titleName)) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(titleName);
}
return builder.toString();
}
@Override
public String toString() {
return String.format(
"type: %d, company: %s, department: %s, title: %s, isPrimary: %s",
type, companyName, departmentName, titleName, isPrimary);
}
}
static public class ImData {
public final int protocol;
public final String customProtocol;
public final int type;
public final String data;
public final boolean isPrimary;
public ImData(final int protocol, final String customProtocol, final int type,
final String data, final boolean isPrimary) {
this.protocol = protocol;
this.customProtocol = customProtocol;
this.type = type;
this.data = data;
this.isPrimary = isPrimary;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ImData)) {
return false;
}
ImData imData = (ImData)obj;
return (type == imData.type && protocol == imData.protocol
&& (customProtocol != null ? customProtocol.equals(imData.customProtocol) :
(imData.customProtocol == null))
&& (data != null ? data.equals(imData.data) : (imData.data == null))
&& isPrimary == imData.isPrimary);
}
@Override
public String toString() {
return String.format(
"type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s",
type, protocol, customProtocol, data, isPrimary);
}
}
public static class PhotoData {
public static final String FORMAT_FLASH = "SWF";
public final int type;
public final String formatName; // used when type is not defined in ContactsContract.
public final byte[] photoBytes;
public final boolean isPrimary;
public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) {
this.type = type;
this.formatName = formatName;
this.photoBytes = photoBytes;
this.isPrimary = isPrimary;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PhotoData)) {
return false;
}
PhotoData photoData = (PhotoData)obj;
return (type == photoData.type &&
(formatName == null ? (photoData.formatName == null) :
formatName.equals(photoData.formatName)) &&
(Arrays.equals(photoBytes, photoData.photoBytes)) &&
(isPrimary == photoData.isPrimary));
}
@Override
public String toString() {
return String.format("type: %d, format: %s: size: %d, isPrimary: %s",
type, formatName, photoBytes.length, isPrimary);
}
}
/* package */ static class Property {
private String mPropertyName;
private Map<String, Collection<String>> mParameterMap =
new HashMap<String, Collection<String>>();
private List<String> mPropertyValueList = new ArrayList<String>();
private byte[] mPropertyBytes;
public void setPropertyName(final String propertyName) {
mPropertyName = propertyName;
}
public void addParameter(final String paramName, final String paramValue) {
Collection<String> values;
if (!mParameterMap.containsKey(paramName)) {
if (paramName.equals("TYPE")) {
values = new HashSet<String>();
} else {
values = new ArrayList<String>();
}
mParameterMap.put(paramName, values);
} else {
values = mParameterMap.get(paramName);
}
values.add(paramValue);
}
public void addToPropertyValueList(final String propertyValue) {
mPropertyValueList.add(propertyValue);
}
public void setPropertyBytes(final byte[] propertyBytes) {
mPropertyBytes = propertyBytes;
}
public final Collection<String> getParameters(String type) {
return mParameterMap.get(type);
}
public final List<String> getPropertyValueList() {
return mPropertyValueList;
}
public void clear() {
mPropertyName = null;
mParameterMap.clear();
mPropertyValueList.clear();
mPropertyBytes = null;
}
}
private String mFamilyName;
private String mGivenName;
private String mMiddleName;
private String mPrefix;
private String mSuffix;
// Used only when no family nor given name is found.
private String mFullName;
private String mPhoneticFamilyName;
private String mPhoneticGivenName;
private String mPhoneticMiddleName;
private String mPhoneticFullName;
private List<String> mNickNameList;
private String mDisplayName;
private String mBirthday;
private List<String> mNoteList;
private List<PhoneData> mPhoneList;
private List<EmailData> mEmailList;
private List<PostalData> mPostalList;
private List<OrganizationData> mOrganizationList;
private List<ImData> mImList;
private List<PhotoData> mPhotoList;
private List<String> mWebsiteList;
private List<List<String>> mAndroidCustomPropertyList;
private final int mVCardType;
private final Account mAccount;
public VCardEntry() {
this(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8);
}
public VCardEntry(int vcardType) {
this(vcardType, null);
}
public VCardEntry(int vcardType, Account account) {
mVCardType = vcardType;
mAccount = account;
}
private void addPhone(int type, String data, String label, boolean isPrimary) {
if (mPhoneList == null) {
mPhoneList = new ArrayList<PhoneData>();
}
final StringBuilder builder = new StringBuilder();
final String trimed = data.trim();
final String formattedNumber;
if (type == Phone.TYPE_PAGER) {
formattedNumber = trimed;
} else {
final int length = trimed.length();
for (int i = 0; i < length; i++) {
char ch = trimed.charAt(i);
if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) {
builder.append(ch);
}
}
// Use NANP in default when there's no information about locale.
final int formattingType = (VCardConfig.isJapaneseDevice(mVCardType) ?
PhoneNumberUtils.FORMAT_JAPAN : PhoneNumberUtils.FORMAT_NANP);
formattedNumber = PhoneNumberUtils.formatNumber(builder.toString(), formattingType);
}
PhoneData phoneData = new PhoneData(type, formattedNumber, label, isPrimary);
mPhoneList.add(phoneData);
}
private void addNickName(final String nickName) {
if (mNickNameList == null) {
mNickNameList = new ArrayList<String>();
}
mNickNameList.add(nickName);
}
private void addEmail(int type, String data, String label, boolean isPrimary){
if (mEmailList == null) {
mEmailList = new ArrayList<EmailData>();
}
mEmailList.add(new EmailData(type, data, label, isPrimary));
}
private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){
if (mPostalList == null) {
mPostalList = new ArrayList<PostalData>(0);
}
mPostalList.add(new PostalData(type, propValueList, label, isPrimary));
}
/**
* Should be called via {@link #handleOrgValue(int, List, boolean)} or
* {@link #handleTitleValue(String)}.
*/
private void addNewOrganization(int type, final String companyName,
final String departmentName,
final String titleName, boolean isPrimary) {
if (mOrganizationList == null) {
mOrganizationList = new ArrayList<OrganizationData>();
}
mOrganizationList.add(new OrganizationData(type, companyName,
departmentName, titleName, isPrimary));
}
private static final List<String> sEmptyList =
Collections.unmodifiableList(new ArrayList<String>(0));
/**
* Set "ORG" related values to the appropriate data. If there's more than one
* {@link OrganizationData} objects, this input data are attached to the last one which
* does not have valid values (not including empty but only null). If there's no
* {@link OrganizationData} object, a new {@link OrganizationData} is created,
* whose title is set to null.
*/
private void handleOrgValue(final int type, List<String> orgList, boolean isPrimary) {
if (orgList == null) {
orgList = sEmptyList;
}
final String companyName;
final String departmentName;
final int size = orgList.size();
switch (size) {
case 0: {
companyName = "";
departmentName = null;
break;
}
case 1: {
companyName = orgList.get(0);
departmentName = null;
break;
}
default: { // More than 1.
companyName = orgList.get(0);
// We're not sure which is the correct string for department.
// In order to keep all the data, concatinate the rest of elements.
StringBuilder builder = new StringBuilder();
for (int i = 1; i < size; i++) {
if (i > 1) {
builder.append(' ');
}
builder.append(orgList.get(i));
}
departmentName = builder.toString();
}
}
if (mOrganizationList == null) {
// Create new first organization entry, with "null" title which may be
// added via handleTitleValue().
addNewOrganization(type, companyName, departmentName, null, isPrimary);
return;
}
for (OrganizationData organizationData : mOrganizationList) {
// Not use TextUtils.isEmpty() since ORG was set but the elements might be empty.
// e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null.
if (organizationData.companyName == null &&
organizationData.departmentName == null) {
// Probably the "TITLE" property comes before the "ORG" property via
// handleTitleLine().
organizationData.companyName = companyName;
organizationData.departmentName = departmentName;
organizationData.isPrimary = isPrimary;
return;
}
}
// No OrganizatioData is available. Create another one, with "null" title, which may be
// added via handleTitleValue().
addNewOrganization(type, companyName, departmentName, null, isPrimary);
}
/**
* Set "title" value to the appropriate data. If there's more than one
* OrganizationData objects, this input is attached to the last one which does not
* have valid title value (not including empty but only null). If there's no
* OrganizationData object, a new OrganizationData is created, whose company name is
* set to null.
*/
private void handleTitleValue(final String title) {
if (mOrganizationList == null) {
// Create new first organization entry, with "null" other info, which may be
// added via handleOrgValue().
addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false);
return;
}
for (OrganizationData organizationData : mOrganizationList) {
if (organizationData.titleName == null) {
organizationData.titleName = title;
return;
}
}
// No Organization is available. Create another one, with "null" other info, which may be
// added via handleOrgValue().
addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false);
}
private void addIm(int protocol, String customProtocol, int type,
String propValue, boolean isPrimary) {
if (mImList == null) {
mImList = new ArrayList<ImData>();
}
mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary));
}
private void addNote(final String note) {
if (mNoteList == null) {
mNoteList = new ArrayList<String>(1);
}
mNoteList.add(note);
}
private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) {
if (mPhotoList == null) {
mPhotoList = new ArrayList<PhotoData>(1);
}
final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary);
mPhotoList.add(photoData);
}
@SuppressWarnings("fallthrough")
private void handleNProperty(List<String> elems) {
// Family, Given, Middle, Prefix, Suffix. (1 - 5)
int size;
if (elems == null || (size = elems.size()) < 1) {
return;
}
if (size > 5) {
size = 5;
}
switch (size) {
// fallthrough
case 5: mSuffix = elems.get(4);
case 4: mPrefix = elems.get(3);
case 3: mMiddleName = elems.get(2);
case 2: mGivenName = elems.get(1);
default: mFamilyName = elems.get(0);
}
}
/**
* Note: Some Japanese mobile phones use this field for phonetic name,
* since vCard 2.1 does not have "SORT-STRING" type.
* Also, in some cases, the field has some ';'s in it.
* Assume the ';' means the same meaning in N property
*/
@SuppressWarnings("fallthrough")
private void handlePhoneticNameFromSound(List<String> elems) {
if (!(TextUtils.isEmpty(mPhoneticFamilyName) &&
TextUtils.isEmpty(mPhoneticMiddleName) &&
TextUtils.isEmpty(mPhoneticGivenName))) {
// This means the other properties like "X-PHONETIC-FIRST-NAME" was already found.
// Ignore "SOUND;X-IRMC-N".
return;
}
int size;
if (elems == null || (size = elems.size()) < 1) {
return;
}
// Assume that the order is "Family, Given, Middle".
// This is not from specification but mere assumption. Some Japanese phones use this order.
if (size > 3) {
size = 3;
}
if (elems.get(0).length() > 0) {
boolean onlyFirstElemIsNonEmpty = true;
for (int i = 1; i < size; i++) {
if (elems.get(i).length() > 0) {
onlyFirstElemIsNonEmpty = false;
break;
}
}
if (onlyFirstElemIsNonEmpty) {
final String[] namesArray = elems.get(0).split(" ");
final int nameArrayLength = namesArray.length;
if (nameArrayLength == 3) {
// Assume the string is "Family Middle Given".
mPhoneticFamilyName = namesArray[0];
mPhoneticMiddleName = namesArray[1];
mPhoneticGivenName = namesArray[2];
} else if (nameArrayLength == 2) {
// Assume the string is "Family Given" based on the Japanese mobile
// phones' preference.
mPhoneticFamilyName = namesArray[0];
mPhoneticGivenName = namesArray[1];
} else {
mPhoneticFullName = elems.get(0);
}
return;
}
}
switch (size) {
// fallthrough
case 3: mPhoneticMiddleName = elems.get(2);
case 2: mPhoneticGivenName = elems.get(1);
default: mPhoneticFamilyName = elems.get(0);
}
}
public void addProperty(final Property property) {
final String propName = property.mPropertyName;
final Map<String, Collection<String>> paramMap = property.mParameterMap;
final List<String> propValueList = property.mPropertyValueList;
byte[] propBytes = property.mPropertyBytes;
if (propValueList.size() == 0) {
return;
}
final String propValue = listToString(propValueList).trim();
if (propName.equals(VCardConstants.PROPERTY_VERSION)) {
// vCard version. Ignore this.
} else if (propName.equals(VCardConstants.PROPERTY_FN)) {
mFullName = propValue;
} else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFullName == null) {
// Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not
// actually exist in the real vCard data, does not exist.
mFullName = propValue;
} else if (propName.equals(VCardConstants.PROPERTY_N)) {
handleNProperty(propValueList);
} else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) {
mPhoneticFullName = propValue;
} else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) ||
propName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) {
addNickName(propValue);
} else if (propName.equals(VCardConstants.PROPERTY_SOUND)) {
Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
if (typeCollection != null
&& typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) {
// As of 2009-10-08, Parser side does not split a property value into separated
// values using ';' (in other words, propValueList.size() == 1),
// which is correct behavior from the view of vCard 2.1.
// But we want it to be separated, so do the separation here.
final List<String> phoneticNameList =
VCardUtils.constructListFromValue(propValue,
VCardConfig.isV30(mVCardType));
handlePhoneticNameFromSound(phoneticNameList);
} else {
// Ignore this field since Android cannot understand what it is.
}
} else if (propName.equals(VCardConstants.PROPERTY_ADR)) {
boolean valuesAreAllEmpty = true;
for (String value : propValueList) {
if (value.length() > 0) {
valuesAreAllEmpty = false;
break;
}
}
if (valuesAreAllEmpty) {
return;
}
int type = -1;
String label = "";
boolean isPrimary = false;
Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
if (typeCollection != null) {
for (String typeString : typeCollection) {
typeString = typeString.toUpperCase();
if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) {
type = StructuredPostal.TYPE_HOME;
label = "";
} else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK) ||
typeString.equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) {
// "COMPANY" seems emitted by Windows Mobile, which is not
// specifically supported by vCard 2.1. We assume this is same
// as "WORK".
type = StructuredPostal.TYPE_WORK;
label = "";
} else if (typeString.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) ||
typeString.equals(VCardConstants.PARAM_ADR_TYPE_DOM) ||
typeString.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) {
// We do not have any appropriate way to store this information.
} else {
if (typeString.startsWith("X-") && type < 0) {
typeString = typeString.substring(2);
}
// vCard 3.0 allows iana-token. Also some vCard 2.1 exporters
// emit non-standard types. We do not handle their values now.
type = StructuredPostal.TYPE_CUSTOM;
label = typeString;
}
}
}
// We use "HOME" as default
if (type < 0) {
type = StructuredPostal.TYPE_HOME;
}
addPostal(type, propValueList, label, isPrimary);
} else if (propName.equals(VCardConstants.PROPERTY_EMAIL)) {
int type = -1;
String label = null;
boolean isPrimary = false;
Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
if (typeCollection != null) {
for (String typeString : typeCollection) {
typeString = typeString.toUpperCase();
if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) {
type = Email.TYPE_HOME;
} else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK)) {
type = Email.TYPE_WORK;
} else if (typeString.equals(VCardConstants.PARAM_TYPE_CELL)) {
type = Email.TYPE_MOBILE;
} else {
if (typeString.startsWith("X-") && type < 0) {
typeString = typeString.substring(2);
}
// vCard 3.0 allows iana-token.
// We may have INTERNET (specified in vCard spec),
// SCHOOL, etc.
type = Email.TYPE_CUSTOM;
label = typeString;
}
}
}
if (type < 0) {
type = Email.TYPE_OTHER;
}
addEmail(type, propValue, label, isPrimary);
} else if (propName.equals(VCardConstants.PROPERTY_ORG)) {
// vCard specification does not specify other types.
final int type = Organization.TYPE_WORK;
boolean isPrimary = false;
Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
if (typeCollection != null) {
for (String typeString : typeCollection) {
if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
}
}
}
handleOrgValue(type, propValueList, isPrimary);
} else if (propName.equals(VCardConstants.PROPERTY_TITLE)) {
handleTitleValue(propValue);
} else if (propName.equals(VCardConstants.PROPERTY_ROLE)) {
// This conflicts with TITLE. Ignore for now...
// handleTitleValue(propValue);
} else if (propName.equals(VCardConstants.PROPERTY_PHOTO) ||
propName.equals(VCardConstants.PROPERTY_LOGO)) {
Collection<String> paramMapValue = paramMap.get("VALUE");
if (paramMapValue != null && paramMapValue.contains("URL")) {
// Currently we do not have appropriate example for testing this case.
} else {
final Collection<String> typeCollection = paramMap.get("TYPE");
String formatName = null;
boolean isPrimary = false;
if (typeCollection != null) {
for (String typeValue : typeCollection) {
if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) {
isPrimary = true;
} else if (formatName == null){
formatName = typeValue;
}
}
}
addPhotoBytes(formatName, propBytes, isPrimary);
}
} else if (propName.equals(VCardConstants.PROPERTY_TEL)) {
final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
final Object typeObject =
VCardUtils.getPhoneTypeFromStrings(typeCollection, propValue);
final int type;
final String label;
if (typeObject instanceof Integer) {
type = (Integer)typeObject;
label = null;
} else {
type = Phone.TYPE_CUSTOM;
label = typeObject.toString();
}
final boolean isPrimary;
if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else {
isPrimary = false;
}
addPhone(type, propValue, label, isPrimary);
} else if (propName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) {
// The phone number available via Skype.
Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
final int type = Phone.TYPE_OTHER;
final boolean isPrimary;
if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else {
isPrimary = false;
}
addPhone(type, propValue, null, isPrimary);
} else if (sImMap.containsKey(propName)) {
final int protocol = sImMap.get(propName);
boolean isPrimary = false;
int type = -1;
final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
if (typeCollection != null) {
for (String typeString : typeCollection) {
if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else if (type < 0) {
if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) {
type = Im.TYPE_HOME;
} else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) {
type = Im.TYPE_WORK;
}
}
}
}
if (type < 0) {
type = Phone.TYPE_HOME;
}
addIm(protocol, null, type, propValue, isPrimary);
} else if (propName.equals(VCardConstants.PROPERTY_NOTE)) {
addNote(propValue);
} else if (propName.equals(VCardConstants.PROPERTY_URL)) {
if (mWebsiteList == null) {
mWebsiteList = new ArrayList<String>(1);
}
mWebsiteList.add(propValue);
} else if (propName.equals(VCardConstants.PROPERTY_BDAY)) {
mBirthday = propValue;
} else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) {
mPhoneticGivenName = propValue;
} else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) {
mPhoneticMiddleName = propValue;
} else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) {
mPhoneticFamilyName = propValue;
} else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
final List<String> customPropertyList =
VCardUtils.constructListFromValue(propValue,
VCardConfig.isV30(mVCardType));
handleAndroidCustomProperty(customPropertyList);
/*} else if (propName.equals("REV")) {
// Revision of this VCard entry. I think we can ignore this.
} else if (propName.equals("UID")) {
} else if (propName.equals("KEY")) {
// Type is X509 or PGP? I don't know how to handle this...
} else if (propName.equals("MAILER")) {
} else if (propName.equals("TZ")) {
} else if (propName.equals("GEO")) {
} else if (propName.equals("CLASS")) {
// vCard 3.0 only.
// e.g. CLASS:CONFIDENTIAL
} else if (propName.equals("PROFILE")) {
// VCard 3.0 only. Must be "VCARD". I think we can ignore this.
} else if (propName.equals("CATEGORIES")) {
// VCard 3.0 only.
// e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY
} else if (propName.equals("SOURCE")) {
// VCard 3.0 only.
} else if (propName.equals("PRODID")) {
// VCard 3.0 only.
// To specify the identifier for the product that created
// the vCard object.*/
} else {
// Unknown X- words and IANA token.
}
}
private void handleAndroidCustomProperty(final List<String> customPropertyList) {
if (mAndroidCustomPropertyList == null) {
mAndroidCustomPropertyList = new ArrayList<List<String>>();
}
mAndroidCustomPropertyList.add(customPropertyList);
}
/**
* Construct the display name. The constructed data must not be null.
*/
private void constructDisplayName() {
// FullName (created via "FN" or "NAME" field) is prefered.
if (!TextUtils.isEmpty(mFullName)) {
mDisplayName = mFullName;
} else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) {
mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix);
} else if (!(TextUtils.isEmpty(mPhoneticFamilyName) &&
TextUtils.isEmpty(mPhoneticGivenName))) {
mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
mPhoneticFamilyName, mPhoneticMiddleName, mPhoneticGivenName);
} else if (mEmailList != null && mEmailList.size() > 0) {
mDisplayName = mEmailList.get(0).data;
} else if (mPhoneList != null && mPhoneList.size() > 0) {
mDisplayName = mPhoneList.get(0).data;
} else if (mPostalList != null && mPostalList.size() > 0) {
mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType);
} else if (mOrganizationList != null && mOrganizationList.size() > 0) {
mDisplayName = mOrganizationList.get(0).getFormattedString();
}
if (mDisplayName == null) {
mDisplayName = "";
}
}
/**
* Consolidate several fielsds (like mName) using name candidates,
*/
public void consolidateFields() {
constructDisplayName();
if (mPhoneticFullName != null) {
mPhoneticFullName = mPhoneticFullName.trim();
}
}
public Uri pushIntoContentResolver(ContentResolver resolver) {
ArrayList<ContentProviderOperation> operationList =
new ArrayList<ContentProviderOperation>();
// After applying the batch the first result's Uri is returned so it is important that
// the RawContact is the first operation that gets inserted into the list
ContentProviderOperation.Builder builder =
ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
String myGroupsId = null;
if (mAccount != null) {
builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
// Assume that caller side creates this group if it does not exist.
if (ACCOUNT_TYPE_GOOGLE.equals(mAccount.type)) {
final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] {
Groups.SOURCE_ID },
Groups.TITLE + "=?", new String[] {
GOOGLE_MY_CONTACTS_GROUP }, null);
try {
if (cursor != null && cursor.moveToFirst()) {
myGroupsId = cursor.getString(0);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
} else {
builder.withValue(RawContacts.ACCOUNT_NAME, null);
builder.withValue(RawContacts.ACCOUNT_TYPE, null);
}
operationList.add(builder.build());
if (!nameFieldsAreEmpty()) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
builder.withValue(StructuredName.GIVEN_NAME, mGivenName);
builder.withValue(StructuredName.FAMILY_NAME, mFamilyName);
builder.withValue(StructuredName.MIDDLE_NAME, mMiddleName);
builder.withValue(StructuredName.PREFIX, mPrefix);
builder.withValue(StructuredName.SUFFIX, mSuffix);
if (!(TextUtils.isEmpty(mPhoneticGivenName)
&& TextUtils.isEmpty(mPhoneticFamilyName)
&& TextUtils.isEmpty(mPhoneticMiddleName))) {
builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName);
builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName);
builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName);
} else if (!TextUtils.isEmpty(mPhoneticFullName)) {
builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticFullName);
}
builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName());
operationList.add(builder.build());
}
if (mNickNameList != null && mNickNameList.size() > 0) {
for (String nickName : mNickNameList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
builder.withValue(Nickname.NAME, nickName);
operationList.add(builder.build());
}
}
if (mPhoneList != null) {
for (PhoneData phoneData : mPhoneList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
builder.withValue(Phone.TYPE, phoneData.type);
if (phoneData.type == Phone.TYPE_CUSTOM) {
builder.withValue(Phone.LABEL, phoneData.label);
}
builder.withValue(Phone.NUMBER, phoneData.data);
if (phoneData.isPrimary) {
builder.withValue(Phone.IS_PRIMARY, 1);
}
operationList.add(builder.build());
}
}
if (mOrganizationList != null) {
for (OrganizationData organizationData : mOrganizationList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
builder.withValue(Organization.TYPE, organizationData.type);
if (organizationData.companyName != null) {
builder.withValue(Organization.COMPANY, organizationData.companyName);
}
if (organizationData.departmentName != null) {
builder.withValue(Organization.DEPARTMENT, organizationData.departmentName);
}
if (organizationData.titleName != null) {
builder.withValue(Organization.TITLE, organizationData.titleName);
}
if (organizationData.isPrimary) {
builder.withValue(Organization.IS_PRIMARY, 1);
}
operationList.add(builder.build());
}
}
if (mEmailList != null) {
for (EmailData emailData : mEmailList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Email.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
builder.withValue(Email.TYPE, emailData.type);
if (emailData.type == Email.TYPE_CUSTOM) {
builder.withValue(Email.LABEL, emailData.label);
}
builder.withValue(Email.DATA, emailData.data);
if (emailData.isPrimary) {
builder.withValue(Data.IS_PRIMARY, 1);
}
operationList.add(builder.build());
}
}
if (mPostalList != null) {
for (PostalData postalData : mPostalList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
VCardUtils.insertStructuredPostalDataUsingContactsStruct(
mVCardType, builder, postalData);
operationList.add(builder.build());
}
}
if (mImList != null) {
for (ImData imData : mImList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Im.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
builder.withValue(Im.TYPE, imData.type);
builder.withValue(Im.PROTOCOL, imData.protocol);
if (imData.protocol == Im.PROTOCOL_CUSTOM) {
builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol);
}
if (imData.isPrimary) {
builder.withValue(Data.IS_PRIMARY, 1);
}
}
}
if (mNoteList != null) {
for (String note : mNoteList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Note.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
builder.withValue(Note.NOTE, note);
operationList.add(builder.build());
}
}
if (mPhotoList != null) {
for (PhotoData photoData : mPhotoList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
builder.withValue(Photo.PHOTO, photoData.photoBytes);
if (photoData.isPrimary) {
builder.withValue(Photo.IS_PRIMARY, 1);
}
operationList.add(builder.build());
}
}
if (mWebsiteList != null) {
for (String website : mWebsiteList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Website.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
builder.withValue(Website.URL, website);
// There's no information about the type of URL in vCard.
// We use TYPE_HOMEPAGE for safety.
builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
operationList.add(builder.build());
}
}
if (!TextUtils.isEmpty(mBirthday)) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Event.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
builder.withValue(Event.START_DATE, mBirthday);
builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
operationList.add(builder.build());
}
if (mAndroidCustomPropertyList != null) {
for (List<String> customPropertyList : mAndroidCustomPropertyList) {
int size = customPropertyList.size();
if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) {
continue;
} else if (size > VCardConstants.MAX_DATA_COLUMN + 1) {
size = VCardConstants.MAX_DATA_COLUMN + 1;
customPropertyList =
customPropertyList.subList(0, VCardConstants.MAX_DATA_COLUMN + 2);
}
int i = 0;
for (final String customPropertyValue : customPropertyList) {
if (i == 0) {
final String mimeType = customPropertyValue;
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, mimeType);
} else { // 1 <= i && i <= MAX_DATA_COLUMNS
if (!TextUtils.isEmpty(customPropertyValue)) {
builder.withValue("data" + i, customPropertyValue);
}
}
i++;
}
operationList.add(builder.build());
}
}
if (myGroupsId != null) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
builder.withValue(GroupMembership.GROUP_SOURCE_ID, myGroupsId);
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(
ContactsContract.AUTHORITY, operationList);
// the first result is always the raw_contact. return it's uri so
// that it can be found later. do null checking for badly behaving
// ContentResolvers
return (results == null || results.length == 0 || results[0] == null)
? null
: results[0].uri;
} catch (RemoteException e) {
Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
} catch (OperationApplicationException e) {
Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
}
}
public static VCardEntry buildFromResolver(ContentResolver resolver) {
return buildFromResolver(resolver, Contacts.CONTENT_URI);
}
public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) {
return null;
}
private boolean nameFieldsAreEmpty() {
return (TextUtils.isEmpty(mFamilyName)
&& TextUtils.isEmpty(mMiddleName)
&& TextUtils.isEmpty(mGivenName)
&& TextUtils.isEmpty(mPrefix)
&& TextUtils.isEmpty(mSuffix)
&& TextUtils.isEmpty(mFullName)
&& TextUtils.isEmpty(mPhoneticFamilyName)
&& TextUtils.isEmpty(mPhoneticMiddleName)
&& TextUtils.isEmpty(mPhoneticGivenName)
&& TextUtils.isEmpty(mPhoneticFullName));
}
public boolean isIgnorable() {
return getDisplayName().length() == 0;
}
private String listToString(List<String> list){
final int size = list.size();
if (size > 1) {
StringBuilder builder = new StringBuilder();
int i = 0;
for (String type : list) {
builder.append(type);
if (i < size - 1) {
builder.append(";");
}
}
return builder.toString();
} else if (size == 1) {
return list.get(0);
} else {
return "";
}
}
// All getter methods should be used carefully, since they may change
// in the future as of 2009-10-05, on which I cannot be sure this structure
// is completely consolidated.
//
// Also note that these getter methods should be used only after
// all properties being pushed into this object. If not, incorrect
// value will "be stored in the local cache and" be returned to you.
public String getFamilyName() {
return mFamilyName;
}
public String getGivenName() {
return mGivenName;
}
public String getMiddleName() {
return mMiddleName;
}
public String getPrefix() {
return mPrefix;
}
public String getSuffix() {
return mSuffix;
}
public String getFullName() {
return mFullName;
}
public String getPhoneticFamilyName() {
return mPhoneticFamilyName;
}
public String getPhoneticGivenName() {
return mPhoneticGivenName;
}
public String getPhoneticMiddleName() {
return mPhoneticMiddleName;
}
public String getPhoneticFullName() {
return mPhoneticFullName;
}
public final List<String> getNickNameList() {
return mNickNameList;
}
public String getBirthday() {
return mBirthday;
}
public final List<String> getNotes() {
return mNoteList;
}
public final List<PhoneData> getPhoneList() {
return mPhoneList;
}
public final List<EmailData> getEmailList() {
return mEmailList;
}
public final List<PostalData> getPostalList() {
return mPostalList;
}
public final List<OrganizationData> getOrganizationList() {
return mOrganizationList;
}
public final List<ImData> getImList() {
return mImList;
}
public final List<PhotoData> getPhotoList() {
return mPhotoList;
}
public final List<String> getWebsiteList() {
return mWebsiteList;
}
public String getDisplayName() {
if (mDisplayName == null) {
constructDisplayName();
}
return mDisplayName;
}
}