You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
etesync-android/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java

1085 lines
36 KiB

/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.resource;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Entity;
import android.content.EntityIterator;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.CommonDataKinds.Email;
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.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.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.util.Log;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import at.bitfire.davdroid.syncadapter.AccountSettings;
import at.bitfire.davdroid.webdav.WebDavResource;
import ezvcard.VCardVersion;
import ezvcard.parameter.AddressType;
import ezvcard.parameter.EmailType;
import ezvcard.parameter.ImppType;
import ezvcard.parameter.RelatedType;
import ezvcard.parameter.TelephoneType;
import ezvcard.property.Address;
import ezvcard.property.Anniversary;
import ezvcard.property.Birthday;
import ezvcard.property.DateOrTimeProperty;
import ezvcard.property.Impp;
import ezvcard.property.Related;
import ezvcard.property.Telephone;
import lombok.Cleanup;
public class LocalAddressBook extends LocalCollection<Contact> {
private final static String TAG = "davdroid.resource";
protected final static String COLUMN_UNKNOWN_PROPERTIES = RawContacts.SYNC3;
final protected AccountSettings accountSettings;
/* database fields */
@Override
protected Uri entriesURI() {
return syncAdapterURI(RawContacts.CONTENT_URI);
}
protected String entryColumnAccountType() { return RawContacts.ACCOUNT_TYPE; }
protected String entryColumnAccountName() { return RawContacts.ACCOUNT_NAME; }
protected String entryColumnParentID() { return null; /* maybe use RawContacts.DATA_SET some day? */ }
protected String entryColumnID() { return RawContacts._ID; }
protected String entryColumnRemoteName() { return RawContacts.SOURCE_ID; }
protected String entryColumnETag() { return RawContacts.SYNC2; }
protected String entryColumnDirty() { return RawContacts.DIRTY; }
protected String entryColumnDeleted() { return RawContacts.DELETED; }
protected String entryColumnUID() { return RawContacts.SYNC1; }
public LocalAddressBook(Account account, ContentProviderClient providerClient, AccountSettings accountSettings) {
super(account, providerClient);
this.accountSettings = accountSettings;
}
/* collection operations */
@Override
public long getId() {
return -1;
}
@Override
public String getCTag() {
return accountSettings.getAddressBookCTag();
}
@Override
public void setCTag(String cTag) {
accountSettings.setAddressBookCTag(cTag);
}
@Override
public void updateMetaData(WebDavResource.Properties properties)
{
final VCardVersion vCardVersion = properties.getSupportedVCardVersion();
accountSettings.setAddressBookVCardVersion(vCardVersion != null ? vCardVersion : VCardVersion.V3_0);
}
/* create/update/delete */
public Contact newResource(long localID, String resourceName, String eTag) {
Contact c = new Contact(localID, resourceName, eTag);
c.vCardVersion = accountSettings.getAddressBookVCardVersion();
return c;
}
@Override
public int commit() throws LocalStorageException {
int affected = super.commit();
// update group details for groups we have just created
Uri groupsUri = syncAdapterURI(Groups.CONTENT_URI);
try {
// newly created groups don't have a TITLE
@Cleanup Cursor cursor = providerClient.query(groupsUri,
new String[]{Groups.SOURCE_ID},
Groups.TITLE + " IS NULL", null, null
);
while (cursor != null && cursor.moveToNext()) {
// found group, set TITLE to SOURCE_ID and other details
String sourceID = cursor.getString(0);
pendingOperations.add(ContentProviderOperation.newUpdate(groupsUri)
.withSelection(Groups.SOURCE_ID + "=?", new String[] { sourceID })
.withValue(Groups.TITLE, sourceID)
.withValue(Groups.GROUP_VISIBLE, 1)
.build());
affected += super.commit();
}
} catch (RemoteException e) {
throw new LocalStorageException("Couldn't update group names", e);
}
return affected;
}
/* methods for populating the data object from the content provider */
@Override
public void populate(Resource res) throws LocalStorageException {
Contact c = (Contact)res;
try {
@Cleanup EntityIterator iter = ContactsContract.RawContacts.newEntityIterator(providerClient.query(
syncAdapterURI(ContactsContract.RawContactsEntity.CONTENT_URI),
null, RawContacts._ID + "=" + c.getLocalID(),
null, null));
if (iter.hasNext()) {
Entity e = iter.next();
ContentValues values = e.getEntityValues();
c.setUid(values.getAsString(entryColumnUID()));
c.unknownProperties = values.getAsString(COLUMN_UNKNOWN_PROPERTIES);
c.starred = values.getAsInteger(RawContacts.STARRED) != 0;
List<Entity.NamedContentValues> subValues = e.getSubValues();
for (Entity.NamedContentValues subValue : subValues) {
values = subValue.values;
String mimeType = values.getAsString(ContactsContract.RawContactsEntity.MIMETYPE);
switch (mimeType) {
case StructuredName.CONTENT_ITEM_TYPE:
populateStructuredName(c, values);
break;
case Phone.CONTENT_ITEM_TYPE:
populatePhoneNumber(c, values);
break;
case Email.CONTENT_ITEM_TYPE:
populateEmailAddress(c, values);
break;
case Photo.CONTENT_ITEM_TYPE:
populatePhoto(c, values);
break;
case Organization.CONTENT_ITEM_TYPE:
populateOrganization(c, values);
break;
case Im.CONTENT_ITEM_TYPE:
populateIMPP(c, values);
break;
case Nickname.CONTENT_ITEM_TYPE:
populateNickname(c, values);
break;
case Note.CONTENT_ITEM_TYPE:
populateNote(c, values);
break;
case StructuredPostal.CONTENT_ITEM_TYPE:
populatePostalAddress(c, values);
break;
case GroupMembership.CONTENT_ITEM_TYPE:
populateGroupMembership(c, values);
break;
case Website.CONTENT_ITEM_TYPE:
populateURL(c, values);
break;
case CommonDataKinds.Event.CONTENT_ITEM_TYPE:
populateEvent(c, values);
break;
case Relation.CONTENT_ITEM_TYPE:
populateRelation(c, values);
break;
case SipAddress.CONTENT_ITEM_TYPE:
populateSipAddress(c, values);
break;
}
}
} else
throw new RecordNotFoundException();
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
private void populateStructuredName(Contact c, ContentValues row) {
c.displayName = row.getAsString(StructuredName.DISPLAY_NAME);
c.prefix = row.getAsString(StructuredName.PREFIX);
c.givenName = row.getAsString(StructuredName.GIVEN_NAME);
c.middleName = row.getAsString(StructuredName.MIDDLE_NAME);
c.familyName = row.getAsString(StructuredName.FAMILY_NAME);
c.suffix = row.getAsString(StructuredName.SUFFIX);
c.phoneticGivenName = row.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
c.phoneticMiddleName = row.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
c.phoneticFamilyName = row.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
}
protected void populatePhoneNumber(Contact c, ContentValues row) {
ezvcard.property.Telephone number = new ezvcard.property.Telephone(row.getAsString(Phone.NUMBER));
switch (row.getAsInteger(Phone.TYPE)) {
case Phone.TYPE_HOME:
number.addType(TelephoneType.HOME);
break;
case Phone.TYPE_MOBILE:
number.addType(TelephoneType.CELL);
break;
case Phone.TYPE_WORK:
number.addType(TelephoneType.WORK);
break;
case Phone.TYPE_FAX_WORK:
number.addType(TelephoneType.FAX);
number.addType(TelephoneType.WORK);
break;
case Phone.TYPE_FAX_HOME:
number.addType(TelephoneType.FAX);
number.addType(TelephoneType.HOME);
break;
case Phone.TYPE_PAGER:
number.addType(TelephoneType.PAGER);
break;
case Phone.TYPE_CALLBACK:
number.addType(Contact.PHONE_TYPE_CALLBACK);
break;
case Phone.TYPE_CAR:
number.addType(TelephoneType.CAR);
break;
case Phone.TYPE_COMPANY_MAIN:
number.addType(Contact.PHONE_TYPE_COMPANY_MAIN);
break;
case Phone.TYPE_ISDN:
number.addType(TelephoneType.ISDN);
break;
case Phone.TYPE_MAIN:
number.addType(TelephoneType.PREF);
break;
case Phone.TYPE_OTHER_FAX:
number.addType(TelephoneType.FAX);
break;
case Phone.TYPE_RADIO:
number.addType(Contact.PHONE_TYPE_RADIO);
break;
case Phone.TYPE_TELEX:
number.addType(TelephoneType.TEXTPHONE);
break;
case Phone.TYPE_TTY_TDD:
number.addType(TelephoneType.TEXT);
break;
case Phone.TYPE_WORK_MOBILE:
number.addType(TelephoneType.CELL);
number.addType(TelephoneType.WORK);
break;
case Phone.TYPE_WORK_PAGER:
number.addType(TelephoneType.PAGER);
number.addType(TelephoneType.WORK);
break;
case Phone.TYPE_ASSISTANT:
number.addType(Contact.PHONE_TYPE_ASSISTANT);
break;
case Phone.TYPE_MMS:
number.addType(Contact.PHONE_TYPE_MMS);
break;
case Phone.TYPE_CUSTOM:
String customType = row.getAsString(Phone.LABEL);
if (StringUtils.isNotEmpty(customType))
number.addType(TelephoneType.get(labelToXName(customType)));
}
if (row.getAsInteger(Phone.IS_PRIMARY) != 0)
number.addType(TelephoneType.PREF);
c.getPhoneNumbers().add(number);
}
protected void populateEmailAddress(Contact c, ContentValues row) {
ezvcard.property.Email email = new ezvcard.property.Email(row.getAsString(Email.ADDRESS));
if (row.containsKey(Email.TYPE))
switch (row.getAsInteger(Email.TYPE)) {
case Email.TYPE_HOME:
email.addType(EmailType.HOME);
break;
case Email.TYPE_WORK:
email.addType(EmailType.WORK);
break;
case Email.TYPE_MOBILE:
email.addType(Contact.EMAIL_TYPE_MOBILE);
break;
case Email.TYPE_CUSTOM:
String customType = row.getAsString(Email.LABEL);
if (StringUtils.isNotEmpty(customType))
email.addType(EmailType.get(labelToXName(customType)));
}
if (row.getAsInteger(Email.IS_PRIMARY) != 0)
email.addType(EmailType.PREF);
c.getEmails().add(email);
}
protected void populatePhoto(Contact c, ContentValues row) throws RemoteException {
if (row.containsKey(Photo.PHOTO_FILE_ID)) {
Uri photoUri = Uri.withAppendedPath(
ContentUris.withAppendedId(RawContacts.CONTENT_URI, c.getLocalID()),
RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
try {
@Cleanup AssetFileDescriptor fd = providerClient.openAssetFile(photoUri, "r");
@Cleanup InputStream is = fd.createInputStream();
c.photo = IOUtils.toByteArray(is);
} catch(IOException ex) {
Log.w(TAG, "Couldn't read high-res contact photo", ex);
}
} else
c.photo = row.getAsByteArray(Photo.PHOTO);
}
protected void populateOrganization(Contact c, ContentValues row) {
String company = row.getAsString(Organization.COMPANY),
department = row.getAsString(Organization.DEPARTMENT),
title = row.getAsString(Organization.TITLE),
role = row.getAsString(Organization.JOB_DESCRIPTION);
if (StringUtils.isNotEmpty(company) || StringUtils.isNotEmpty(department)) {
ezvcard.property.Organization org = new ezvcard.property.Organization();
if (StringUtils.isNotEmpty(company))
org.addValue(company);
if (StringUtils.isNotEmpty(department))
org.addValue(department);
c.organization = org;
}
if (StringUtils.isNotEmpty(title))
c.jobTitle = title;
if (StringUtils.isNotEmpty(role))
c.jobDescription = role;
}
protected void populateIMPP(Contact c, ContentValues row) {
String handle = row.getAsString(Im.DATA);
Impp impp = null;
switch (row.getAsInteger(Im.PROTOCOL)) {
case Im.PROTOCOL_AIM:
impp = Impp.aim(handle);
break;
case Im.PROTOCOL_MSN:
impp = Impp.msn(handle);
break;
case Im.PROTOCOL_YAHOO:
impp = Impp.yahoo(handle);
break;
case Im.PROTOCOL_SKYPE:
impp = Impp.skype(handle);
break;
case Im.PROTOCOL_QQ:
impp = new Impp("qq", handle);
break;
case Im.PROTOCOL_GOOGLE_TALK:
impp = new Impp("google-talk", handle);
break;
case Im.PROTOCOL_ICQ:
impp = Impp.icq(handle);
break;
case Im.PROTOCOL_JABBER:
impp = Impp.xmpp(handle);
break;
case Im.PROTOCOL_NETMEETING:
impp = new Impp("netmeeting", handle);
break;
case Im.PROTOCOL_CUSTOM:
impp = new Impp(row.getAsString(Im.CUSTOM_PROTOCOL), handle);
}
if (impp != null) {
if (row.containsKey(Im.TYPE))
switch (row.getAsInteger(Im.TYPE)) {
case Im.TYPE_HOME:
impp.addType(ImppType.HOME);
break;
case Im.TYPE_WORK:
impp.addType(ImppType.WORK);
break;
case Im.TYPE_CUSTOM:
String customType = row.getAsString(Im.LABEL);
if (StringUtils.isNotEmpty(customType))
impp.addType(ImppType.get(labelToXName(customType)));
}
c.getImpps().add(impp);
}
}
protected void populateNickname(Contact c, ContentValues row) {
// TYPE (maiden name, short name, …) and LABEL are not processed here because Contacts app doesn't support it
c.nickName = row.getAsString(Nickname.NAME);
}
protected void populateNote(Contact c, ContentValues row) {
c.note = row.getAsString(Note.NOTE);
}
protected void populatePostalAddress(Contact c, ContentValues row) {
Address address = new Address();
address.setLabel(row.getAsString(StructuredPostal.FORMATTED_ADDRESS));
if (row.containsKey(StructuredPostal.TYPE))
switch (row.getAsInteger(StructuredPostal.TYPE)) {
case StructuredPostal.TYPE_HOME:
address.addType(AddressType.HOME);
break;
case StructuredPostal.TYPE_WORK:
address.addType(AddressType.WORK);
break;
case StructuredPostal.TYPE_CUSTOM:
String customType = row.getAsString(StructuredPostal.LABEL);
if (StringUtils.isNotEmpty(customType))
address.addType(AddressType.get(labelToXName(customType)));
break;
}
address.setStreetAddress(row.getAsString(StructuredPostal.STREET));
address.setPoBox(row.getAsString(StructuredPostal.POBOX));
address.setExtendedAddress(row.getAsString(StructuredPostal.NEIGHBORHOOD));
address.setLocality(row.getAsString(StructuredPostal.CITY));
address.setRegion(row.getAsString(StructuredPostal.REGION));
address.setPostalCode(row.getAsString(StructuredPostal.POSTCODE));
address.setCountry(row.getAsString(StructuredPostal.COUNTRY));
c.getAddresses().add(address);
}
protected void populateGroupMembership(Contact c, ContentValues row) throws RemoteException {
List<String> categories = c.getCategories();
long rowID = row.getAsLong(GroupMembership.GROUP_ROW_ID);
String sourceID = row.getAsString(GroupMembership.GROUP_SOURCE_ID);
// either a row ID or a source ID must be available
String where, whereArg;
if (sourceID == null) {
where = Groups._ID + "=?";
whereArg = String.valueOf(rowID);
} else {
where = Groups.SOURCE_ID + "=?";
whereArg = sourceID;
}
where += " AND " + Groups.DELETED + "=0"; // ignore deleted groups
Log.d(TAG, "Populating group from " + where + " " + whereArg);
// fetch group
@Cleanup Cursor cursorGroups = providerClient.query(Groups.CONTENT_URI,
new String[] { Groups.TITLE },
where, new String[] { whereArg }, null
);
if (cursorGroups != null && cursorGroups.moveToNext()) {
String title = cursorGroups.getString(0);
if (sourceID == null) { // Group wasn't created by DAVdroid
// SOURCE_ID IS NULL <=> _ID IS NOT NULL
Log.d(TAG, "Setting SOURCE_ID of non-DAVdroid group to title: " + title);
ContentValues v = new ContentValues(1);
v.put(Groups.SOURCE_ID, title);
v.put(Groups.GROUP_IS_READ_ONLY, 0);
v.put(Groups.GROUP_VISIBLE, 1);
providerClient.update(syncAdapterURI(Groups.CONTENT_URI), v, Groups._ID + "=?", new String[] { String.valueOf(rowID) });
sourceID = title;
}
// add group to CATEGORIES
if (sourceID != null)
categories.add(sourceID);
} else
Log.d(TAG, "Group not found (maybe deleted)");
}
protected void populateURL(Contact c, ContentValues row) {
c.getURLs().add(row.getAsString(Website.URL));
}
protected void populateEvent(Contact c, ContentValues row) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
try {
Date date = formatter.parse(row.getAsString(CommonDataKinds.Event.START_DATE));
switch (row.getAsInteger(CommonDataKinds.Event.TYPE)) {
case CommonDataKinds.Event.TYPE_ANNIVERSARY:
c.anniversary = new Anniversary(date);
break;
case CommonDataKinds.Event.TYPE_BIRTHDAY:
c.birthDay = new Birthday(date);
break;
}
} catch (ParseException e) {
Log.w(TAG, "Couldn't parse local birthday/anniversary date", e);
}
}
protected void populateRelation(Contact c, ContentValues row) {
String name = row.getAsString(Relation.NAME);
// don't process empty relations
if (StringUtils.isEmpty(name))
return;
// find relation by name or create new one
Related related = null;
for (Related rel : c.getRelations()) {
if (name.equals(rel.getText())) {
related = rel;
break;
}
}
if (related == null) {
related = new Related();
c.getRelations().add(related);
}
Set<RelatedType> types = related.getTypes();
switch (row.getAsInteger(Relation.TYPE)) {
case Relation.TYPE_ASSISTANT:
types.add(RelatedType.AGENT);
break;
case Relation.TYPE_BROTHER:
types.add(RelatedType.SIBLING);
types.add(Contact.RELATED_TYPE_BROTHER);
break;
case Relation.TYPE_CHILD:
types.add(RelatedType.CHILD);
break;
case Relation.TYPE_DOMESTIC_PARTNER:
types.add(RelatedType.CO_RESIDENT);
break;
case Relation.TYPE_FATHER:
types.add(Contact.RELATED_TYPE_FATHER);
break;
case Relation.TYPE_FRIEND:
types.add(RelatedType.FRIEND);
break;
case Relation.TYPE_MANAGER:
types.add(Contact.RELATED_TYPE_MANAGER);
break;
case Relation.TYPE_MOTHER:
types.add(Contact.RELATED_TYPE_MOTHER);
break;
case Relation.TYPE_PARENT:
types.add(RelatedType.PARENT);
break;
case Relation.TYPE_PARTNER:
types.add(RelatedType.SWEETHEART);
break;
case Relation.TYPE_REFERRED_BY:
types.add(Contact.RELATED_TYPE_REFERRED_BY);
case Relation.TYPE_RELATIVE:
types.add(RelatedType.KIN);
break;
case Relation.TYPE_SISTER:
types.add(RelatedType.SIBLING);
types.add(Contact.RELATED_TYPE_SISTER);
break;
case Relation.TYPE_SPOUSE:
types.add(RelatedType.SPOUSE);
case Relation.TYPE_CUSTOM:
String customType = row.getAsString(Relation.LABEL);
if (StringUtils.isNotEmpty(customType))
types.add(RelatedType.get(customType));
}
}
protected void populateSipAddress(Contact c, ContentValues row) {
try {
Impp impp = new Impp("sip:" + row.getAsString(SipAddress.SIP_ADDRESS));
if (row.containsKey(SipAddress.TYPE))
switch (row.getAsInteger(SipAddress.TYPE)) {
case SipAddress.TYPE_HOME:
impp.addType(ImppType.HOME);
break;
case SipAddress.TYPE_WORK:
impp.addType(ImppType.WORK);
break;
case SipAddress.TYPE_CUSTOM:
String customType = row.getAsString(SipAddress.LABEL);
if (StringUtils.isNotEmpty(customType))
impp.addType(ImppType.get(labelToXName(customType)));
}
c.getImpps().add(impp);
} catch(IllegalArgumentException e) {
Log.e(TAG, "Illegal SIP URI", e);
}
}
/* content builder methods */
@Override
protected Builder buildEntry(Builder builder, Resource resource, boolean update) {
Contact contact = (Contact)resource;
if (!update)
builder .withValue(RawContacts.ACCOUNT_NAME, account.name)
.withValue(RawContacts.ACCOUNT_TYPE, account.type);
return builder
.withValue(entryColumnRemoteName(), contact.getName())
.withValue(entryColumnUID(), contact.getUid())
.withValue(entryColumnETag(), contact.getETag())
.withValue(COLUMN_UNKNOWN_PROPERTIES, contact.unknownProperties)
.withValue(RawContacts.STARRED, contact.starred ? 1 : 0);
}
@Override
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
Contact contact = (Contact)resource;
queueOperation(buildStructuredName(newDataInsertBuilder(localID, backrefIdx), contact));
for (Telephone number : contact.getPhoneNumbers())
queueOperation(buildPhoneNumber(newDataInsertBuilder(localID, backrefIdx), number));
for (ezvcard.property.Email email : contact.getEmails())
queueOperation(buildEmail(newDataInsertBuilder(localID, backrefIdx), email));
if (contact.photo != null)
queueOperation(buildPhoto(newDataInsertBuilder(localID, backrefIdx), contact.photo));
queueOperation(buildOrganization(newDataInsertBuilder(localID, backrefIdx), contact));
for (Impp impp : contact.getImpps())
queueOperation(buildIMPP(newDataInsertBuilder(localID, backrefIdx), impp));
if (contact.nickName != null)
queueOperation(buildNickName(newDataInsertBuilder(localID, backrefIdx), contact.nickName));
if (contact.note != null)
queueOperation(buildNote(newDataInsertBuilder(localID, backrefIdx), contact.note));
for (Address address : contact.getAddresses())
queueOperation(buildAddress(newDataInsertBuilder(localID, backrefIdx), address));
for (String category : contact.getCategories())
queueOperation(buildGroupMembership(newDataInsertBuilder(localID, backrefIdx), category));
for (String url : contact.getURLs())
queueOperation(buildURL(newDataInsertBuilder(localID, backrefIdx), url));
if (contact.anniversary != null)
queueOperation(buildEvent(newDataInsertBuilder(localID, backrefIdx), contact.anniversary, CommonDataKinds.Event.TYPE_ANNIVERSARY));
if (contact.birthDay != null)
queueOperation(buildEvent(newDataInsertBuilder(localID, backrefIdx), contact.birthDay, CommonDataKinds.Event.TYPE_BIRTHDAY));
for (Related related : contact.getRelations())
for (RelatedType type : related.getTypes())
queueOperation(buildRelated(newDataInsertBuilder(localID, backrefIdx), type, related.getText()));
// SIP addresses are built by buildIMPP
}
@Override
protected void removeDataRows(Resource resource) {
pendingOperations.add(ContentProviderOperation.newDelete(dataURI())
.withSelection(Data.RAW_CONTACT_ID + "=?",
new String[] { String.valueOf(resource.getLocalID()) }).build());
}
protected Builder buildStructuredName(Builder builder, Contact contact) {
return builder
.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
.withValue(StructuredName.PREFIX, contact.prefix)
.withValue(StructuredName.DISPLAY_NAME, contact.displayName)
.withValue(StructuredName.GIVEN_NAME, contact.givenName)
.withValue(StructuredName.MIDDLE_NAME, contact.middleName)
.withValue(StructuredName.FAMILY_NAME, contact.familyName)
.withValue(StructuredName.SUFFIX, contact.suffix)
.withValue(StructuredName.PHONETIC_GIVEN_NAME, contact.phoneticGivenName)
.withValue(StructuredName.PHONETIC_MIDDLE_NAME, contact.phoneticMiddleName)
.withValue(StructuredName.PHONETIC_FAMILY_NAME, contact.phoneticFamilyName);
}
protected Builder buildPhoneNumber(Builder builder, Telephone number) {
int typeCode = Phone.TYPE_OTHER;
String typeLabel = null;
Set<TelephoneType> types = number.getTypes();
// preferred number?
boolean is_primary = false;
if (types.contains(TelephoneType.PREF)) {
is_primary = true;
types.remove(TelephoneType.PREF);
}
// 1 Android type <-> 2 VCard types: fax, cell, pager
if (types.contains(TelephoneType.FAX)) {
if (types.contains(TelephoneType.HOME))
typeCode = Phone.TYPE_FAX_HOME;
else if (types.contains(TelephoneType.WORK))
typeCode = Phone.TYPE_FAX_WORK;
else
typeCode = Phone.TYPE_OTHER_FAX;
} else if (types.contains(TelephoneType.CELL)) {
if (types.contains(TelephoneType.WORK))
typeCode = Phone.TYPE_WORK_MOBILE;
else
typeCode = Phone.TYPE_MOBILE;
} else if (types.contains(TelephoneType.PAGER)) {
if (types.contains(TelephoneType.WORK))
typeCode = Phone.TYPE_WORK_PAGER;
else
typeCode = Phone.TYPE_PAGER;
// types with 1:1 translation
} else if (types.contains(TelephoneType.HOME)) {
typeCode = Phone.TYPE_HOME;
} else if (types.contains(TelephoneType.WORK)) {
typeCode = Phone.TYPE_WORK;
} else if (types.contains(Contact.PHONE_TYPE_CALLBACK)) {
typeCode = Phone.TYPE_CALLBACK;
} else if (types.contains(TelephoneType.CAR)) {
typeCode = Phone.TYPE_CAR;
} else if (types.contains(Contact.PHONE_TYPE_COMPANY_MAIN)) {
typeCode = Phone.TYPE_COMPANY_MAIN;
} else if (types.contains(TelephoneType.ISDN)) {
typeCode = Phone.TYPE_ISDN;
} else if (types.contains(Contact.PHONE_TYPE_RADIO)) {
typeCode = Phone.TYPE_RADIO;
} else if (types.contains(TelephoneType.TEXTPHONE)) {
typeCode = Phone.TYPE_TELEX;
} else if (types.contains(TelephoneType.TEXT)) {
typeCode = Phone.TYPE_TTY_TDD;
} else if (types.contains(Contact.PHONE_TYPE_ASSISTANT)) {
typeCode = Phone.TYPE_ASSISTANT;
} else if (types.contains(Contact.PHONE_TYPE_MMS)) {
typeCode = Phone.TYPE_MMS;
} else if (!types.isEmpty()) {
TelephoneType type = types.iterator().next();
typeCode = Phone.TYPE_CUSTOM;
typeLabel = xNameToLabel(type.getValue());
}
builder .withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE)
.withValue(Phone.NUMBER, number.getText())
.withValue(Phone.TYPE, typeCode)
.withValue(Phone.IS_PRIMARY, is_primary ? 1 : 0)
.withValue(Phone.IS_SUPER_PRIMARY, is_primary ? 1 : 0);
if (typeLabel != null)
builder.withValue(Phone.LABEL, typeLabel);
return builder;
}
protected Builder buildEmail(Builder builder, ezvcard.property.Email email) {
int typeCode = 0;
String typeLabel = null;
boolean is_primary = false;
for (EmailType type : email.getTypes())
if (type == EmailType.PREF)
is_primary = true;
else if (type == EmailType.HOME)
typeCode = Email.TYPE_HOME;
else if (type == EmailType.WORK)
typeCode = Email.TYPE_WORK;
else if (type == Contact.EMAIL_TYPE_MOBILE)
typeCode = Email.TYPE_MOBILE;
if (typeCode == 0) {
if (email.getTypes().isEmpty())
typeCode = Email.TYPE_OTHER;
else {
typeCode = Email.TYPE_CUSTOM;
typeLabel = xNameToLabel(email.getTypes().iterator().next().getValue());
}
}
builder .withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE)
.withValue(Email.ADDRESS, email.getValue())
.withValue(Email.TYPE, typeCode)
.withValue(Email.IS_PRIMARY, is_primary ? 1 : 0)
.withValue(Phone.IS_SUPER_PRIMARY, is_primary ? 1 : 0);
if (typeLabel != null)
builder.withValue(Email.LABEL, typeLabel);
return builder;
}
protected Builder buildPhoto(Builder builder, byte[] photo) {
return builder
.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE)
.withValue(Photo.PHOTO, photo);
}
protected Builder buildOrganization(Builder builder, Contact contact) {
if (contact.organization == null && contact.jobTitle == null && contact.jobDescription == null)
return null;
ezvcard.property.Organization organization = contact.organization;
String company = null, department = null;
if (organization != null) {
Iterator<String> org = organization.getValues().iterator();
if (org.hasNext())
company = org.next();
if (org.hasNext())
department = org.next();
}
return builder
.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE)
.withValue(Organization.COMPANY, company)
.withValue(Organization.DEPARTMENT, department)
.withValue(Organization.TITLE, contact.jobTitle)
.withValue(Organization.JOB_DESCRIPTION, contact.jobDescription);
}
protected Builder buildIMPP(Builder builder, Impp impp) {
int typeCode = 0;
String typeLabel = null;
for (ImppType type : impp.getTypes())
if (type == ImppType.HOME)
typeCode = Im.TYPE_HOME;
else if (type == ImppType.WORK || type == ImppType.BUSINESS)
typeCode = Im.TYPE_WORK;
if (typeCode == 0)
if (impp.getTypes().isEmpty())
typeCode = Im.TYPE_OTHER;
else {
typeCode = Im.TYPE_CUSTOM;
typeLabel = xNameToLabel(impp.getTypes().iterator().next().getValue());
}
int protocolCode = 0;
String protocolLabel = null;
String protocol = impp.getProtocol();
if (protocol == null) {
Log.w(TAG, "Ignoring IMPP address without protocol");
return null;
}
// SIP addresses are IMPP entries in the VCard but locally stored in SipAddress rather than Im
boolean sipAddress = false;
if (impp.isAim())
protocolCode = Im.PROTOCOL_AIM;
else if (impp.isMsn())
protocolCode = Im.PROTOCOL_MSN;
else if (impp.isYahoo())
protocolCode = Im.PROTOCOL_YAHOO;
else if (impp.isSkype())
protocolCode = Im.PROTOCOL_SKYPE;
else if (protocol.equalsIgnoreCase("qq"))
protocolCode = Im.PROTOCOL_QQ;
else if (protocol.equalsIgnoreCase("google-talk"))
protocolCode = Im.PROTOCOL_GOOGLE_TALK;
else if (impp.isIcq())
protocolCode = Im.PROTOCOL_ICQ;
else if (impp.isXmpp() || protocol.equalsIgnoreCase("jabber"))
protocolCode = Im.PROTOCOL_JABBER;
else if (protocol.equalsIgnoreCase("netmeeting"))
protocolCode = Im.PROTOCOL_NETMEETING;
else if (protocol.equalsIgnoreCase("sip"))
sipAddress = true;
else {
protocolCode = Im.PROTOCOL_CUSTOM;
protocolLabel = protocol;
}
if (sipAddress)
// save as SIP address
builder .withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE)
.withValue(Im.DATA, impp.getHandle())
.withValue(Im.TYPE, typeCode);
else {
// save as IM address
builder .withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE)
.withValue(Im.DATA, impp.getHandle())
.withValue(Im.TYPE, typeCode)
.withValue(Im.PROTOCOL, protocolCode);
if (protocolLabel != null)
builder.withValue(Im.CUSTOM_PROTOCOL, protocolLabel);
}
if (typeLabel != null)
builder.withValue(Im.LABEL, typeLabel);
return builder;
}
protected Builder buildNickName(Builder builder, String nickName) {
return builder
.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE)
.withValue(Nickname.NAME, nickName);
}
protected Builder buildNote(Builder builder, String note) {
return builder
.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE)
.withValue(Note.NOTE, note);
}
protected Builder buildAddress(Builder builder, Address address) {
/* street po.box (extended)
* region
* postal code city
* country
*/
String formattedAddress = address.getLabel();
if (StringUtils.isEmpty(formattedAddress)) {
String lineStreet = StringUtils.join(new String[] { address.getStreetAddress(), address.getPoBox(), address.getExtendedAddress() }, " "),
lineLocality = StringUtils.join(new String[] { address.getPostalCode(), address.getLocality() }, " ");
List<String> lines = new LinkedList<>();
if (StringUtils.isNotBlank(lineStreet))
lines.add(lineStreet);
if (address.getRegion() != null && !address.getRegion().isEmpty())
lines.add(address.getRegion());
if (StringUtils.isNotBlank(lineLocality))
lines.add(lineLocality);
formattedAddress = StringUtils.join(lines, "\n");
}
int typeCode = 0;
String typeLabel = null;
for (AddressType type : address.getTypes())
if (type == AddressType.HOME)
typeCode = StructuredPostal.TYPE_HOME;
else if (type == AddressType.WORK)
typeCode = StructuredPostal.TYPE_WORK;
if (typeCode == 0)
if (address.getTypes().isEmpty())
typeCode = StructuredPostal.TYPE_OTHER;
else {
typeCode = StructuredPostal.TYPE_CUSTOM;
typeLabel = xNameToLabel(address.getTypes().iterator().next().getValue());
}
builder .withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE)
.withValue(StructuredPostal.FORMATTED_ADDRESS, formattedAddress)
.withValue(StructuredPostal.TYPE, typeCode)
.withValue(StructuredPostal.STREET, address.getStreetAddress())
.withValue(StructuredPostal.POBOX, address.getPoBox())
.withValue(StructuredPostal.NEIGHBORHOOD, address.getExtendedAddress())
.withValue(StructuredPostal.CITY, address.getLocality())
.withValue(StructuredPostal.REGION, address.getRegion())
.withValue(StructuredPostal.POSTCODE, address.getPostalCode())
.withValue(StructuredPostal.COUNTRY, address.getCountry());
if (typeLabel != null)
builder.withValue(StructuredPostal.LABEL, typeLabel);
return builder;
}
protected Builder buildGroupMembership(Builder builder, String group) {
return builder
.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
.withValue(GroupMembership.GROUP_SOURCE_ID, group);
}
protected Builder buildURL(Builder builder, String url) {
return builder
.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE)
.withValue(Website.URL, url);
}
protected Builder buildEvent(Builder builder, DateOrTimeProperty date, int type) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
if (date.getDate() == null) {
Log.i(TAG, "Ignoring contact event without date");
return null;
}
return builder
.withValue(Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE)
.withValue(CommonDataKinds.Event.TYPE, type)
.withValue(CommonDataKinds.Event.START_DATE, formatter.format(date.getDate()));
}
protected Builder buildRelated(Builder builder, RelatedType type, String name) {
int typeCode;
String typeLabel = null;
if (type == RelatedType.CHILD)
typeCode = Relation.TYPE_CHILD;
else if (type == RelatedType.CO_RESIDENT)
typeCode = Relation.TYPE_DOMESTIC_PARTNER;
else if (type == RelatedType.FRIEND)
typeCode = Relation.TYPE_FRIEND;
else if (type == RelatedType.PARENT)
typeCode = Relation.TYPE_PARENT;
else if (type == RelatedType.SPOUSE)
typeCode = Relation.TYPE_SPOUSE;
else if (type == RelatedType.KIN)
typeCode = Relation.TYPE_RELATIVE;
else if (type == RelatedType.SWEETHEART)
typeCode = Relation.TYPE_PARTNER;
else {
typeCode = Relation.TYPE_CUSTOM;
typeLabel = type.getValue();
}
return builder
.withValue(Data.MIMETYPE, Relation.CONTENT_ITEM_TYPE)
.withValue(Relation.TYPE, typeCode)
.withValue(Relation.NAME, name)
.withValue(Relation.LABEL, typeLabel);
}
/* helper methods */
protected Uri dataURI() {
return syncAdapterURI(Data.CONTENT_URI);
}
protected static String labelToXName(String label) {
return "X-" + label.replaceAll(" ","_").replaceAll("[^\\p{L}\\p{Nd}\\-_]", "").toUpperCase(Locale.US);
}
private Builder newDataInsertBuilder(long raw_contact_id, Integer backrefIdx) {
return newDataInsertBuilder(dataURI(), Data.RAW_CONTACT_ID, raw_contact_id, backrefIdx);
}
protected static String xNameToLabel(String xname) {
// "X-MY_PROPERTY"
// 1. ensure lower case -> "x-my_property"
// 2. remove x- from beginning -> "my_property"
// 3. replace "_" by " " -> "my property"
// 4. capitalize -> "My Property"
String lowerCase = StringUtils.lowerCase(xname, Locale.US),
withoutPrefix = StringUtils.removeStart(lowerCase, "x-"),
withSpaces = StringUtils.replace(withoutPrefix, "_", " ");
return StringUtils.capitalize(withSpaces);
}
}