am 5a016488: Merge commit \'refs/changes/08/29008/8\' of ssh://dmiyakawa@android-git.corp.google.com:29418/platform/frameworks/base into cleanup_import

Merge commit '5a016488da5ff6c1ecdff0b1febd836cc1711531' into eclair-mr2-plus-aosp

* commit '5a016488da5ff6c1ecdff0b1febd836cc1711531':
  Develop ContentResolver-based unit tests for vCard importer and fix vCard code
This commit is contained in:
Daisuke Miyakawa
2009-10-08 20:09:31 -07:00
committed by Android Git Automerger
12 changed files with 1686 additions and 1112 deletions

View File

@@ -38,6 +38,7 @@ import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website; import android.provider.ContactsContract.CommonDataKinds.Website;
import android.telephony.PhoneNumberUtils; import android.telephony.PhoneNumberUtils;
import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@@ -46,7 +47,6 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -90,7 +90,7 @@ public class ContactStruct {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof PhoneData) { if (!(obj instanceof PhoneData)) {
return false; return false;
} }
PhoneData phoneData = (PhoneData)obj; PhoneData phoneData = (PhoneData)obj;
@@ -125,7 +125,7 @@ public class ContactStruct {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof EmailData) { if (!(obj instanceof EmailData)) {
return false; return false;
} }
EmailData emailData = (EmailData)obj; EmailData emailData = (EmailData)obj;
@@ -202,7 +202,7 @@ public class ContactStruct {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof PostalData) { if (!(obj instanceof PostalData)) {
return false; return false;
} }
PostalData postalData = (PostalData)obj; PostalData postalData = (PostalData)obj;
@@ -256,35 +256,44 @@ public class ContactStruct {
*/ */
static public class OrganizationData { static public class OrganizationData {
public final int type; public final int type;
public final String companyName; // non-final is Intended: we may change the values since this info is separated into
// can be changed in some VCard format. // two parts in vCard: "ORG" + "TITLE".
public String positionName; public String companyName;
public String departmentName;
public String titleName;
// isPrimary is changable only when there's no appropriate one existing in // isPrimary is changable only when there's no appropriate one existing in
// the original VCard. // the original VCard.
public boolean isPrimary; public boolean isPrimary;
public OrganizationData(int type, String companyName, String positionName, public OrganizationData(int type,
String companyName,
String departmentName,
String titleName,
boolean isPrimary) { boolean isPrimary) {
this.type = type; this.type = type;
this.companyName = companyName; this.companyName = companyName;
this.positionName = positionName; this.departmentName = departmentName;
this.titleName = titleName;
this.isPrimary = isPrimary; this.isPrimary = isPrimary;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof OrganizationData) { if (!(obj instanceof OrganizationData)) {
return false; return false;
} }
OrganizationData organization = (OrganizationData)obj; OrganizationData organization = (OrganizationData)obj;
return (type == organization.type && companyName.equals(organization.companyName) && return (type == organization.type &&
positionName.equals(organization.positionName) && TextUtils.equals(companyName, organization.companyName) &&
TextUtils.equals(departmentName, organization.departmentName) &&
TextUtils.equals(titleName, organization.titleName) &&
isPrimary == organization.isPrimary); isPrimary == organization.isPrimary);
} }
@Override @Override
public String toString() { public String toString() {
return String.format("type: %d, company: %s, position: %s, isPrimary: %s", return String.format(
type, companyName, positionName, isPrimary); "type: %d, company: %s, department: %s, title: %s, isPrimary: %s",
type, companyName, departmentName, titleName, isPrimary);
} }
} }
@@ -304,7 +313,7 @@ public class ContactStruct {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof ImData) { if (!(obj instanceof ImData)) {
return false; return false;
} }
ImData imData = (ImData)obj; ImData imData = (ImData)obj;
@@ -327,11 +336,32 @@ public class ContactStruct {
public final int type; public final int type;
public final String formatName; // used when type is not defined in ContactsContract. public final String formatName; // used when type is not defined in ContactsContract.
public final byte[] photoBytes; public final byte[] photoBytes;
public final boolean isPrimary;
public PhotoData(int type, String formatName, byte[] photoBytes) { public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) {
this.type = type; this.type = type;
this.formatName = formatName; this.formatName = formatName;
this.photoBytes = photoBytes; 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);
} }
} }
@@ -421,15 +451,6 @@ public class ContactStruct {
private final int mVCardType; private final int mVCardType;
private final Account mAccount; private final Account mAccount;
// Each Column of four properties has ISPRIMARY field
// (See android.provider.Contacts)
// If false even after the parsing loop, we choose the first entry as a "primary"
// entry.
private boolean mPrefIsSet_Address;
private boolean mPrefIsSet_Phone;
private boolean mPrefIsSet_Email;
private boolean mPrefIsSet_Organization;
public ContactStruct() { public ContactStruct() {
this(VCardConfig.VCARD_TYPE_V21_GENERIC); this(VCardConfig.VCARD_TYPE_V21_GENERIC);
} }
@@ -454,12 +475,14 @@ public class ContactStruct {
String phoneticGivenName, String phoneticGivenName,
String pheneticFamilyName, String pheneticFamilyName,
String phoneticMiddleName, String phoneticMiddleName,
List<String> nicknameList,
List<byte[]> photoBytesList, List<byte[]> photoBytesList,
List<String> notes, List<String> noteList,
List<PhoneData> phoneList, List<PhoneData> phoneList,
List<EmailData> emailList, List<EmailData> emailList,
List<PostalData> postalList, List<PostalData> postalList,
List<OrganizationData> organizationList, List<OrganizationData> organizationList,
List<ImData> imList,
List<PhotoData> photoList, List<PhotoData> photoList,
List<String> websiteList) { List<String> websiteList) {
this(VCardConfig.VCARD_TYPE_DEFAULT); this(VCardConfig.VCARD_TYPE_DEFAULT);
@@ -470,159 +493,16 @@ public class ContactStruct {
mPhoneticGivenName = givenName; mPhoneticGivenName = givenName;
mPhoneticFamilyName = familyName; mPhoneticFamilyName = familyName;
mPhoneticMiddleName = middleName; mPhoneticMiddleName = middleName;
mNickNameList = nicknameList;
mNoteList = noteList;
mEmailList = emailList; mEmailList = emailList;
mPostalList = postalList; mPostalList = postalList;
mOrganizationList = organizationList; mOrganizationList = organizationList;
mImList = imList;
mPhotoList = photoList; mPhotoList = photoList;
mWebsiteList = websiteList; mWebsiteList = websiteList;
} }
// All getter methods should be used carefully, since they may change
// in the future as of 2009-09-24, on which I cannot be sure this structure
// is completely consolidated.
// When we are sure we will no longer change them, we'll be happy to
// make it complete public (withouth @hide tag)
//
// 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.
/**
* @hide
*/
public String getFamilyName() {
return mFamilyName;
}
/**
* @hide
*/
public String getGivenName() {
return mGivenName;
}
/**
* @hide
*/
public String getMiddleName() {
return mMiddleName;
}
/**
* @hide
*/
public String getPrefix() {
return mPrefix;
}
/**
* @hide
*/
public String getSuffix() {
return mSuffix;
}
/**
* @hide
*/
public String getFullName() {
return mFullName;
}
/**
* @hide
*/
public String getPhoneticFamilyName() {
return mPhoneticFamilyName;
}
/**
* @hide
*/
public String getPhoneticGivenName() {
return mPhoneticGivenName;
}
/**
* @hide
*/
public String getPhoneticMiddleName() {
return mPhoneticMiddleName;
}
/**
* @hide
*/
public String getPhoneticFullName() {
return mPhoneticFullName;
}
/**
* @hide
*/
public final List<String> getNickNameList() {
return mNickNameList;
}
/**
* @hide
*/
public String getDisplayName() {
if (mDisplayName == null) {
constructDisplayName();
}
return mDisplayName;
}
/**
* @hide
*/
public String getBirthday() {
return mBirthday;
}
/**
* @hide
*/
public final List<PhotoData> getPhotoList() {
return mPhotoList;
}
/**
* @hide
*/
public final List<String> getNotes() {
return mNoteList;
}
/**
* @hide
*/
public final List<PhoneData> getPhoneList() {
return mPhoneList;
}
/**
* @hide
*/
public final List<EmailData> getEmailList() {
return mEmailList;
}
/**
* @hide
*/
public final List<PostalData> getPostalList() {
return mPostalList;
}
/**
* @hide
*/
public final List<OrganizationData> getOrganizationList() {
return mOrganizationList;
}
/** /**
* Add a phone info to phoneList. * Add a phone info to phoneList.
* @param data phone number * @param data phone number
@@ -643,10 +523,24 @@ public class ContactStruct {
} }
} }
PhoneData phoneData = new PhoneData(type, final String formattedPhoneNumber;
PhoneNumberUtils.formatNumber(builder.toString()), {
label, isPrimary); final String rawPhoneNumber = builder.toString();
if (VCardConfig.isJapaneseDevice(mVCardType)) {
// As of 2009-10-07, there's no formatNumber() which accepts
// the second argument and returns String directly.
final SpannableStringBuilder tmpBuilder =
new SpannableStringBuilder(rawPhoneNumber);
PhoneNumberUtils.formatNumber(tmpBuilder, PhoneNumberUtils.FORMAT_JAPAN);
formattedPhoneNumber = tmpBuilder.toString();
} else {
// There's no information available on vCard side. Depend on the default
// behavior, which may cause problem in the future when the additional format
// rule is supported (e.g. PhoneNumberUtils.FORMAT_KLINGON)
formattedPhoneNumber = PhoneNumberUtils.formatNumber(rawPhoneNumber);
}
}
PhoneData phoneData = new PhoneData(type, formattedPhoneNumber, label, isPrimary);
mPhoneList.add(phoneData); mPhoneList.add(phoneData);
} }
@@ -666,17 +560,114 @@ public class ContactStruct {
private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){ private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){
if (mPostalList == null) { if (mPostalList == null) {
mPostalList = new ArrayList<PostalData>(); mPostalList = new ArrayList<PostalData>(0);
} }
mPostalList.add(new PostalData(type, propValueList, label, isPrimary)); mPostalList.add(new PostalData(type, propValueList, label, isPrimary));
} }
private void addOrganization(int type, final String companyName, /**
final String positionName, boolean 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) { if (mOrganizationList == null) {
mOrganizationList = new ArrayList<OrganizationData>(); mOrganizationList = new ArrayList<OrganizationData>();
} }
mOrganizationList.add(new OrganizationData(type, companyName, positionName, isPrimary)); mOrganizationList.add(new OrganizationData(type, companyName,
departmentName, titleName, isPrimary));
}
private static final List<String> sEmptyList = new ArrayList<String>(0);
/**
* Set "ORG" related values to the appropriate data. If there's more than one
* 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
* OrganizationData object, a new 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);
}
private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK;
/**
* 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 type, String data, String label, boolean isPrimary) { private void addIm(int type, String data, String label, boolean isPrimary) {
@@ -693,43 +684,14 @@ public class ContactStruct {
mNoteList.add(note); mNoteList.add(note);
} }
private void addPhotoBytes(String formatName, byte[] photoBytes) { private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) {
if (mPhotoList == null) { if (mPhotoList == null) {
mPhotoList = new ArrayList<PhotoData>(1); mPhotoList = new ArrayList<PhotoData>(1);
} }
final PhotoData photoData = new PhotoData(0, null, photoBytes); final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary);
mPhotoList.add(photoData); mPhotoList.add(photoData);
} }
/**
* Set "position" value to the appropriate data. If there's more than one
* OrganizationData objects, the value is set to the last one. If there's no
* OrganizationData object, a new OrganizationData is created, whose company name is
* empty.
*
* TODO: incomplete logic. fix this:
*
* e.g. This assumes ORG comes earlier, but TITLE may come earlier like this, though we do not
* know how to handle it in general cases...
* ----
* TITLE:Software Engineer
* ORG:Google
* ----
*/
private void setPosition(String positionValue) {
if (mOrganizationList == null) {
mOrganizationList = new ArrayList<OrganizationData>();
}
int size = mOrganizationList.size();
if (size == 0) {
addOrganization(ContactsContract.CommonDataKinds.Organization.TYPE_OTHER,
"", null, false);
size = 1;
}
OrganizationData lastData = mOrganizationList.get(size - 1);
lastData.positionName = positionValue;
}
@SuppressWarnings("fallthrough") @SuppressWarnings("fallthrough")
private void handleNProperty(List<String> elems) { private void handleNProperty(List<String> elems) {
// Family, Given, Middle, Prefix, Suffix. (1 - 5) // Family, Given, Middle, Prefix, Suffix. (1 - 5)
@@ -813,7 +775,14 @@ public class ContactStruct {
} else if (propName.equals("SOUND")) { } else if (propName.equals("SOUND")) {
Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE);
if (typeCollection != null && typeCollection.contains(Constants.ATTR_TYPE_X_IRMC_N)) { if (typeCollection != null && typeCollection.contains(Constants.ATTR_TYPE_X_IRMC_N)) {
handlePhoneticNameFromSound(propValueList); // 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 { } else {
// Ignore this field since Android cannot understand what it is. // Ignore this field since Android cannot understand what it is.
} }
@@ -836,9 +805,7 @@ public class ContactStruct {
if (typeCollection != null) { if (typeCollection != null) {
for (String typeString : typeCollection) { for (String typeString : typeCollection) {
typeString = typeString.toUpperCase(); typeString = typeString.toUpperCase();
if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Address) { if (typeString.equals(Constants.ATTR_TYPE_PREF)) {
// Only first "PREF" is considered.
mPrefIsSet_Address = true;
isPrimary = true; isPrimary = true;
} else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) {
type = StructuredPostal.TYPE_HOME; type = StructuredPostal.TYPE_HOME;
@@ -879,9 +846,7 @@ public class ContactStruct {
if (typeCollection != null) { if (typeCollection != null) {
for (String typeString : typeCollection) { for (String typeString : typeCollection) {
typeString = typeString.toUpperCase(); typeString = typeString.toUpperCase();
if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Email) { if (typeString.equals(Constants.ATTR_TYPE_PREF)) {
// Only first "PREF" is considered.
mPrefIsSet_Email = true;
isPrimary = true; isPrimary = true;
} else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) {
type = Email.TYPE_HOME; type = Email.TYPE_HOME;
@@ -907,48 +872,44 @@ public class ContactStruct {
addEmail(type, propValue, label, isPrimary); addEmail(type, propValue, label, isPrimary);
} else if (propName.equals("ORG")) { } else if (propName.equals("ORG")) {
// vCard specification does not specify other types. // vCard specification does not specify other types.
int type = Organization.TYPE_WORK; final int type = Organization.TYPE_WORK;
boolean isPrimary = false; boolean isPrimary = false;
Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE);
if (typeCollection != null) { if (typeCollection != null) {
for (String typeString : typeCollection) { for (String typeString : typeCollection) {
if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Organization) { if (typeString.equals(Constants.ATTR_TYPE_PREF)) {
// vCard specification officially does not have PREF in ORG.
// This is just for safety.
mPrefIsSet_Organization = true;
isPrimary = true; isPrimary = true;
} }
} }
} }
handleOrgValue(type, propValueList, isPrimary);
StringBuilder builder = new StringBuilder();
for (Iterator<String> iter = propValueList.iterator(); iter.hasNext();) {
builder.append(iter.next());
if (iter.hasNext()) {
builder.append(' ');
}
}
addOrganization(type, builder.toString(), "", isPrimary);
} else if (propName.equals("TITLE")) { } else if (propName.equals("TITLE")) {
setPosition(propValue); handleTitleValue(propValue);
} else if (propName.equals("ROLE")) { } else if (propName.equals("ROLE")) {
setPosition(propValue); // This conflicts with TITLE. Ignore for now...
// handleTitleValue(propValue);
} else if (propName.equals("PHOTO") || propName.equals("LOGO")) { } else if (propName.equals("PHOTO") || propName.equals("LOGO")) {
String formatName = null;
Collection<String> typeCollection = paramMap.get("TYPE");
if (typeCollection != null) {
formatName = typeCollection.iterator().next();
}
Collection<String> paramMapValue = paramMap.get("VALUE"); Collection<String> paramMapValue = paramMap.get("VALUE");
if (paramMapValue != null && paramMapValue.contains("URL")) { if (paramMapValue != null && paramMapValue.contains("URL")) {
// Currently we do not have appropriate example for testing this case. // Currently we do not have appropriate example for testing this case.
} else { } else {
addPhotoBytes(formatName, propBytes); final Collection<String> typeCollection = paramMap.get("TYPE");
String formatName = null;
boolean isPrimary = false;
if (typeCollection != null) {
for (String typeValue : typeCollection) {
if (Constants.ATTR_TYPE_PREF.equals(typeValue)) {
isPrimary = true;
} else if (formatName == null){
formatName = typeValue;
}
}
}
addPhotoBytes(formatName, propBytes, isPrimary);
} }
} else if (propName.equals("TEL")) { } else if (propName.equals("TEL")) {
Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); final Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE);
Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection); final Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection);
final int type; final int type;
final String label; final String label;
if (typeObject instanceof Integer) { if (typeObject instanceof Integer) {
@@ -960,9 +921,7 @@ public class ContactStruct {
} }
final boolean isPrimary; final boolean isPrimary;
if (!mPrefIsSet_Phone && typeCollection != null && if (typeCollection != null && typeCollection.contains(Constants.ATTR_TYPE_PREF)) {
typeCollection.contains(Constants.ATTR_TYPE_PREF)) {
mPrefIsSet_Phone = true;
isPrimary = true; isPrimary = true;
} else { } else {
isPrimary = false; isPrimary = false;
@@ -975,9 +934,7 @@ public class ContactStruct {
int type = Phone.TYPE_OTHER; int type = Phone.TYPE_OTHER;
final String label = null; final String label = null;
final boolean isPrimary; final boolean isPrimary;
if (!mPrefIsSet_Phone && typeCollection != null && if (typeCollection != null && typeCollection.contains(Constants.ATTR_TYPE_PREF)) {
typeCollection.contains(Constants.ATTR_TYPE_PREF)) {
mPrefIsSet_Phone = true;
isPrimary = true; isPrimary = true;
} else { } else {
isPrimary = false; isPrimary = false;
@@ -1048,7 +1005,10 @@ public class ContactStruct {
* Construct the display name. The constructed data must not be null. * Construct the display name. The constructed data must not be null.
*/ */
private void constructDisplayName() { private void constructDisplayName() {
if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { // FullName (created via "FN" or "NAME" field) is prefered.
if (!TextUtils.isEmpty(mFullName)) {
mDisplayName = mFullName;
} else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
List<String> nameList; List<String> nameList;
switch (VCardConfig.getNameOrderType(mVCardType)) { switch (VCardConfig.getNameOrderType(mVCardType)) {
@@ -1079,8 +1039,6 @@ public class ContactStruct {
} }
} }
mDisplayName = builder.toString(); mDisplayName = builder.toString();
} else if (!TextUtils.isEmpty(mFullName)) {
mDisplayName = mFullName;
} else if (!(TextUtils.isEmpty(mPhoneticFamilyName) && } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) &&
TextUtils.isEmpty(mPhoneticGivenName))) { TextUtils.isEmpty(mPhoneticGivenName))) {
mDisplayName = VCardUtils.constructNameFromElements(mVCardType, mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
@@ -1107,21 +1065,6 @@ public class ContactStruct {
if (mPhoneticFullName != null) { if (mPhoneticFullName != null) {
mPhoneticFullName = mPhoneticFullName.trim(); mPhoneticFullName = mPhoneticFullName.trim();
} }
// If there is no "PREF", we choose the first entries as primary.
if (!mPrefIsSet_Phone && mPhoneList != null && mPhoneList.size() > 0) {
mPhoneList.get(0).isPrimary = true;
}
if (!mPrefIsSet_Address && mPostalList != null && mPostalList.size() > 0) {
mPostalList.get(0).isPrimary = true;
}
if (!mPrefIsSet_Email && mEmailList != null && mEmailList.size() > 0) {
mEmailList.get(0).isPrimary = true;
}
if (!mPrefIsSet_Organization && mOrganizationList != null && mOrganizationList.size() > 0) {
mOrganizationList.get(0).isPrimary = true;
}
} }
// From GoogleSource.java in Contacts app. // From GoogleSource.java in Contacts app.
@@ -1181,18 +1124,12 @@ public class ContactStruct {
} }
if (mNickNameList != null && mNickNameList.size() > 0) { if (mNickNameList != null && mNickNameList.size() > 0) {
boolean first = true;
for (String nickName : mNickNameList) { for (String nickName : mNickNameList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0); builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE); builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
builder.withValue(Nickname.NAME, nickName); builder.withValue(Nickname.NAME, nickName);
if (first) {
builder.withValue(Data.IS_PRIMARY, 1);
first = false;
}
operationList.add(builder.build()); operationList.add(builder.build());
} }
} }
@@ -1209,25 +1146,29 @@ public class ContactStruct {
} }
builder.withValue(Phone.NUMBER, phoneData.data); builder.withValue(Phone.NUMBER, phoneData.data);
if (phoneData.isPrimary) { if (phoneData.isPrimary) {
builder.withValue(Data.IS_PRIMARY, 1); builder.withValue(Phone.IS_PRIMARY, 1);
} }
operationList.add(builder.build()); operationList.add(builder.build());
} }
} }
if (mOrganizationList != null) { if (mOrganizationList != null) {
boolean first = true;
for (OrganizationData organizationData : mOrganizationList) { for (OrganizationData organizationData : mOrganizationList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0); builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
// Currently, we do not use TYPE_CUSTOM.
builder.withValue(Organization.TYPE, organizationData.type); builder.withValue(Organization.TYPE, organizationData.type);
builder.withValue(Organization.COMPANY, organizationData.companyName); if (organizationData.companyName != null) {
builder.withValue(Organization.TITLE, organizationData.positionName); builder.withValue(Organization.COMPANY, organizationData.companyName);
if (first) { }
builder.withValue(Data.IS_PRIMARY, 1); 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()); operationList.add(builder.build());
} }
@@ -1265,7 +1206,6 @@ public class ContactStruct {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Im.RAW_CONTACT_ID, 0); builder.withValueBackReference(Im.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
builder.withValue(Im.TYPE, imData.type); builder.withValue(Im.TYPE, imData.type);
if (imData.type == Im.TYPE_CUSTOM) { if (imData.type == Im.TYPE_CUSTOM) {
builder.withValue(Im.LABEL, imData.label); builder.withValue(Im.LABEL, imData.label);
@@ -1282,22 +1222,19 @@ public class ContactStruct {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Note.RAW_CONTACT_ID, 0); builder.withValueBackReference(Note.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
builder.withValue(Note.NOTE, note); builder.withValue(Note.NOTE, note);
operationList.add(builder.build()); operationList.add(builder.build());
} }
} }
if (mPhotoList != null) { if (mPhotoList != null) {
boolean first = true;
for (PhotoData photoData : mPhotoList) { for (PhotoData photoData : mPhotoList) {
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0); builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0);
builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
builder.withValue(Photo.PHOTO, photoData.photoBytes); builder.withValue(Photo.PHOTO, photoData.photoBytes);
if (first) { if (photoData.isPrimary) {
builder.withValue(Data.IS_PRIMARY, 1); builder.withValue(Photo.IS_PRIMARY, 1);
first = false;
} }
operationList.add(builder.build()); operationList.add(builder.build());
} }
@@ -1310,8 +1247,8 @@ public class ContactStruct {
builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE); builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
builder.withValue(Website.URL, website); builder.withValue(Website.URL, website);
// There's no information about the type of URL in vCard. // There's no information about the type of URL in vCard.
// We use TYPE_HOME for safety. // We use TYPE_HOMEPAGE for safety.
builder.withValue(Website.TYPE, Website.TYPE_HOME); builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
operationList.add(builder.build()); operationList.add(builder.build());
} }
} }
@@ -1364,4 +1301,99 @@ public class ContactStruct {
return ""; 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;
}
} }

View File

@@ -41,8 +41,8 @@ public class VCardConfig {
// TODO: make the other codes use this flag // TODO: make the other codes use this flag
public static final boolean IGNORE_CASE_EXCEPT_VALUE = true; public static final boolean IGNORE_CASE_EXCEPT_VALUE = true;
private static final int FLAG_V21 = 0; public static final int FLAG_V21 = 0;
private static final int FLAG_V30 = 1; public static final int FLAG_V30 = 1;
// 0x2 is reserved for the future use ... // 0x2 is reserved for the future use ...
@@ -106,7 +106,15 @@ public class VCardConfig {
*/ */
public static final int FLAG_USE_QP_TO_PRIMARY_PROPERTIES = 0x10000000; public static final int FLAG_USE_QP_TO_PRIMARY_PROPERTIES = 0x10000000;
// VCard types // Note: if you really want to add additional flag(s) incompatible with the main source tree,
// please use flags from 0x0001000 to 0x00080000.
//
// If we notice we cannot manage flags in just one integer, we'll change this interface and
// create/use a complete class, not just an integer, with similar API (but imcompatible).
//
// Again, please be aware that this API is intentionally hidden ~= unstable!
//// The followings are VCard types available from importer/exporter. ////
/** /**
* General vCard format with the version 2.1. Uses UTF-8 for the charset. * General vCard format with the version 2.1. Uses UTF-8 for the charset.

View File

@@ -144,7 +144,11 @@ public class VCardParser_V21 extends VCardParser {
} }
} }
protected String getVersion() { protected int getVersion() {
return VCardConfig.FLAG_V21;
}
protected String getVersionString() {
return "2.1"; return "2.1";
} }
@@ -392,9 +396,10 @@ public class VCardParser_V21 extends VCardParser {
} else { } else {
throw new VCardException("Unknown BEGIN type: " + propertyValue); throw new VCardException("Unknown BEGIN type: " + propertyValue);
} }
} else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersion())) { } else if (propertyName.equals("VERSION") &&
!propertyValue.equals(getVersionString())) {
throw new VCardVersionException("Incompatible version: " + throw new VCardVersionException("Incompatible version: " +
propertyValue + " != " + getVersion()); propertyValue + " != " + getVersionString());
} }
start = System.currentTimeMillis(); start = System.currentTimeMillis();
handlePropertyValue(propertyName, propertyValue); handlePropertyValue(propertyName, propertyValue);
@@ -761,29 +766,8 @@ public class VCardParser_V21 extends VCardParser {
} }
if (mBuilder != null) { if (mBuilder != null) {
StringBuilder builder = new StringBuilder(); mBuilder.propertyValues(VCardUtils.constructListFromValue(
ArrayList<String> list = new ArrayList<String>(); propertyValue, (getVersion() == VCardConfig.FLAG_V30)));
int length = propertyValue.length();
for (int i = 0; i < length; i++) {
char ch = propertyValue.charAt(i);
if (ch == '\\' && i < length - 1) {
char nextCh = propertyValue.charAt(i + 1);
String unescapedString = maybeUnescapeCharacter(nextCh);
if (unescapedString != null) {
builder.append(unescapedString);
i++;
} else {
builder.append(ch);
}
} else if (ch == ';') {
list.add(builder.toString());
builder = new StringBuilder();
} else {
builder.append(ch);
}
}
list.add(builder.toString());
mBuilder.propertyValues(list);
} }
} }
@@ -825,6 +809,10 @@ public class VCardParser_V21 extends VCardParser {
* e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be.
*/ */
protected String maybeUnescapeCharacter(char ch) { protected String maybeUnescapeCharacter(char ch) {
return unescapeCharacter(ch);
}
public static String unescapeCharacter(char ch) {
// Original vCard 2.1 specification does not allow transformation // Original vCard 2.1 specification does not allow transformation
// "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of
// this class allowed them, so keep it as is. // this class allowed them, so keep it as is.

View File

@@ -50,7 +50,12 @@ public class VCardParser_V30 extends VCardParser_V21 {
private boolean mEmittedAgentWarning = false; private boolean mEmittedAgentWarning = false;
@Override @Override
protected String getVersion() { protected int getVersion() {
return VCardConfig.FLAG_V30;
}
@Override
protected String getVersionString() {
return Constants.VERSION_V30; return Constants.VERSION_V30;
} }
@@ -284,6 +289,10 @@ public class VCardParser_V30 extends VCardParser_V21 {
*/ */
@Override @Override
protected String maybeUnescapeText(String text) { protected String maybeUnescapeText(String text) {
return unescapeText(text);
}
public static String unescapeText(String text) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
int length = text.length(); int length = text.length();
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
@@ -304,6 +313,10 @@ public class VCardParser_V30 extends VCardParser_V21 {
@Override @Override
protected String maybeUnescapeCharacter(char ch) { protected String maybeUnescapeCharacter(char ch) {
return unescapeCharacter(ch);
}
public static String unescapeCharacter(char ch) {
if (ch == 'n' || ch == 'N') { if (ch == 'n' || ch == 'N') {
return "\n"; return "\n";
} else { } else {

View File

@@ -22,9 +22,11 @@ import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.text.TextUtils; import android.text.TextUtils;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -187,7 +189,10 @@ public class VCardUtils {
} }
builder.withValue(StructuredPostal.POBOX, postalData.pobox); builder.withValue(StructuredPostal.POBOX, postalData.pobox);
// Extended address is dropped since there's no relevant entry in ContactsContract. // TODO: Japanese phone seems to use this field for expressing all the address including
// region, city, etc. Not sure we're ok to store them into NEIGHBORHOOD, while it would be
// better than dropping them all.
builder.withValue(StructuredPostal.NEIGHBORHOOD, postalData.extendedAddress);
builder.withValue(StructuredPostal.STREET, postalData.street); builder.withValue(StructuredPostal.STREET, postalData.street);
builder.withValue(StructuredPostal.CITY, postalData.localty); builder.withValue(StructuredPostal.CITY, postalData.localty);
builder.withValue(StructuredPostal.REGION, postalData.region); builder.withValue(StructuredPostal.REGION, postalData.region);
@@ -283,6 +288,35 @@ public class VCardUtils {
return builder.toString(); return builder.toString();
} }
public static List<String> constructListFromValue(final String value,
final boolean isV30) {
final List<String> list = new ArrayList<String>();
StringBuilder builder = new StringBuilder();
int length = value.length();
for (int i = 0; i < length; i++) {
char ch = value.charAt(i);
if (ch == '\\' && i < length - 1) {
char nextCh = value.charAt(i + 1);
final String unescapedString =
(isV30 ? VCardParser_V30.unescapeCharacter(nextCh) :
VCardParser_V21.unescapeCharacter(nextCh));
if (unescapedString != null) {
builder.append(unescapedString);
i++;
} else {
builder.append(ch);
}
} else if (ch == ';') {
list.add(builder.toString());
builder = new StringBuilder();
} else {
builder.append(ch);
}
}
list.add(builder.toString());
return list;
}
public static boolean containsOnlyPrintableAscii(String str) { public static boolean containsOnlyPrintableAscii(String str) {
if (TextUtils.isEmpty(str)) { if (TextUtils.isEmpty(str)) {
return true; return true;

View File

@@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:2.1
FN:Normal Guy
ORG:Company;Organization;Devision;Room;Sheet No.
TITLE:Excellent Janitor
END:VCARD

View File

@@ -0,0 +1,15 @@
BEGIN:VCARD
VERSION:2.1
FN:Smith
TEL;HOME:1
TEL;WORK;PREF:2
TEL;ISDN:3
EMAIL;PREF;HOME:test@example.com
EMAIL;CELL;PREF:test2@examination.com
ORG:Company
TITLE:Engineer
ORG:Mystery
TITLE:Blogger
ORG:Poetry
TITLE:Poet
END:VCARD

View File

@@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:2.1
FN:Nice Guy
TITLE:Cool Title
ORG:Marverous;Perfect;Great;Good;Bad;Poor
END:VCARD

View File

@@ -16,6 +16,7 @@
package com.android.unit_tests.vcard; package com.android.unit_tests.vcard;
import android.content.ContentValues; import android.content.ContentValues;
import android.pim.vcard.ContactStruct;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
@@ -28,7 +29,12 @@ import java.util.Map.Entry;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* @hide old class just for test * Previously used in main vCard handling code but now exists only for testing.
*
* Especially useful for testing parser code (VCardParser), since all properties can be
* checked via this class unlike {@link ContactStruct}, which only emits the result of
* interpretation of the content of each vCard. We cannot know whether vCard parser or
* ContactStruct is wrong withouth this class.
*/ */
public class PropertyNode { public class PropertyNode {
public String propName; public String propName;

View File

@@ -18,7 +18,7 @@ package com.android.unit_tests.vcard;
import java.util.ArrayList; import java.util.ArrayList;
/** /**
* @hide old class. Just for testing * Previously used in main vCard handling code but now exists only for testing.
*/ */
public class VNode { public class VNode {
public String VName; public String VName;

View File

@@ -36,7 +36,8 @@ import java.util.List;
* Maybe several vcard instance, so use vNodeList to store. * Maybe several vcard instance, so use vNodeList to store.
* VNode: standy by a vcard instance. * VNode: standy by a vcard instance.
* PropertyNode: standy by a property line of a card. * PropertyNode: standy by a property line of a card.
* @hide old class, just for testing use *
* Previously used in main vCard handling code but now exists only for testing.
*/ */
public class VNodeBuilder implements VCardBuilder { public class VNodeBuilder implements VCardBuilder {
static private String LOG_TAG = "VDATABuilder"; static private String LOG_TAG = "VDATABuilder";