/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at
* src/com/vodafone360/people/VODAFONE.LICENSE.txt or
* http://github.com/360/360-Engine-for-Android
* See the License for the specific language governing permissions and
* limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each file and
* include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt.
* If applicable, add the following below this CDDL HEADER, with the fields
* enclosed by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
* Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved.
* Use is subject to license terms.
*/
package com.vodafone360.people.engine.contactsync;
import java.util.ArrayList;
import java.util.List;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.provider.Contacts;
import android.provider.Contacts.ContactMethods;
import android.provider.Contacts.Organizations;
import android.provider.Contacts.People;
import android.provider.Contacts.Phones;
import android.text.TextUtils;
import android.util.SparseIntArray;
import com.vodafone360.people.datatypes.VCardHelper;
import com.vodafone360.people.datatypes.VCardHelper.Name;
import com.vodafone360.people.datatypes.VCardHelper.Organisation;
import com.vodafone360.people.datatypes.VCardHelper.PostalAddress;
import com.vodafone360.people.utils.CursorUtils;
import com.vodafone360.people.utils.LogUtils;
/**
* The implementation of the NativeContactsApi for the Android 1.X platform.
*/
@SuppressWarnings("deprecation")
// Needed to hide warnings because we are using deprecated 1.X native apis
// APIs
public class NativeContactsApi1 extends NativeContactsApi {
/**
* Convenience Contact ID Projection.
*/
private final static String[] CONTACTID_PROJECTION = {
People._ID
};
/**
* Convenience Projection for the NATE and NOTES from the People table
*/
private final static String[] PEOPLE_PROJECTION = {
People.NAME, People.NOTES
};
/**
* Contact methods table kind value for email.
*/
private static final int CONTACT_METHODS_KIND_EMAIL = 1;
/**
* Contact methods table kind value for postal address.
*/
private static final int CONTACT_METHODS_KIND_ADDRESS = 2;
/**
* Mapping of supported {@link ContactChange} Keys.
*/
private static final SparseIntArray sSupportedKeys;
static {
sSupportedKeys = new SparseIntArray(7);
sSupportedKeys.append(ContactChange.KEY_VCARD_NAME, 0);
sSupportedKeys.append(ContactChange.KEY_VCARD_PHONE, 0);
sSupportedKeys.append(ContactChange.KEY_VCARD_EMAIL, 0);
sSupportedKeys.append(ContactChange.KEY_VCARD_ADDRESS, 0);
sSupportedKeys.append(ContactChange.KEY_VCARD_ORG, 0);
sSupportedKeys.append(ContactChange.KEY_VCARD_TITLE, 0);
sSupportedKeys.append(ContactChange.KEY_VCARD_NOTE, 0);
}
/**
* Values used for writing to NAB
*/
private final ContentValues mValues = new ContentValues();
/**
* The registered ContactsObserver.
*
* @see #registerObserver(ContactsObserver)
*/
private ContactsObserver mContactsObserver;
/**
* The observer for the native contacts.
*/
private ContentObserver mNativeObserver;
/**
* @see NativeContactsApi#initialize()
*/
@Override
protected void initialize() {
// Nothing to do
}
/**
* @see NativeContactsApi#registerObserver(ContactsObserver)
*/
@Override
public void registerObserver(ContactsObserver observer) {
if (mContactsObserver != null) {
throw new RuntimeException(
"NativeContactsApi1.registerObserver(): current implementation only supports one observer"
+ " at a time... Please unregister first.");
}
mContactsObserver = observer;
mNativeObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
mContactsObserver.onChange();
}
};
mCr.registerContentObserver(Contacts.CONTENT_URI, true, mNativeObserver);
}
/**
* @see NativeContactsApi#unregisterObserver()
*/
@Override
public void unregisterObserver() {
if (mContactsObserver != null) {
mContactsObserver = null;
mCr.unregisterContentObserver(mNativeObserver);
mNativeObserver = null;
}
}
/**
* @see NativeContactsApi#getAccounts()
*/
@Override
public Account[] getAccounts() {
// No accounts on 1.X NAB
return null;
}
/**
* @see NativeContactsApi#getAccountsByType(String)
*/
@Override
public Account[] getAccountsByType(int type) {
// No accounts on 1.X NAB
return null;
}
/**
* @see NativeContactsApi#addPeopleAccount(String)
*/
@Override
public boolean addPeopleAccount(String username) {
// No People Accounts on 1.X NAB
return false;
}
/**
* @see NativeContactsApi#isPeopleAccountCreated()
*/
@Override
public boolean isPeopleAccountCreated() {
// No People Accounts on 1.X NAB
return false;
}
/**
* @see NativeContactsApi#removePeopleAccount()
*/
@Override
public void removePeopleAccount() {
// No People Accounts on 1.X NAB
}
/**
* @see NativeContactsApi#getContactIds(Account)
*/
@Override
public long[] getContactIds(Account account) {
if (account != null) {
// Accounts not supported in 1.X, just return null
LogUtils.logE("NativeContactsApi1.getContactIds() Unexpected non-null Account(\""
+ account.toString() + "\"");
return null;
}
long[] ids = null;
final Cursor cursor = mCr.query(People.CONTENT_URI, CONTACTID_PROJECTION, null, null,
People._ID);
try {
final int idCount = cursor.getCount();
if (idCount > 0) {
ids = new long[idCount];
int index = 0;
while (cursor.moveToNext()) {
ids[index] = cursor.getLong(0);
index++;
}
}
} finally {
CursorUtils.closeCursor(cursor);
}
return ids;
}
/**
* @see NativeContactsApi#getContact(long)
*/
@Override
public ContactChange[] getContact(long nabContactId) {
final List<ContactChange> ccList = new ArrayList<ContactChange>();
final Cursor cursor = mCr.query(ContentUris
.withAppendedId(People.CONTENT_URI, nabContactId), PEOPLE_PROJECTION, null, null,
null);
try {
if (cursor.moveToNext()) {
final String displayName = CursorUtils.getString(cursor, People.NAME);
if (!TextUtils.isEmpty(displayName)) {
// TODO: Remove if really not necessary
// final Name name = parseRawName(displayName);
final Name name = new Name();
name.firstname = displayName;
final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_NAME,
VCardHelper.makeName(name), ContactChange.FLAG_NONE);
cc.setNabContactId(nabContactId);
ccList.add(cc);
}
// Note
final String note = CursorUtils.getString(cursor, People.NOTES);
if (!TextUtils.isEmpty(note)) {
final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_NOTE, note,
ContactChange.FLAG_NONE);
cc.setNabContactId(nabContactId);
ccList.add(cc);
}
// Remaining contact details
readContactPhoneNumbers(ccList, nabContactId);
readContactMethods(ccList, nabContactId);
readContactOrganizations(ccList, nabContactId);
}
} finally {
CursorUtils.closeCursor(cursor);
}
return ccList.toArray(new ContactChange[ccList.size()]);
}
/**
* @see NativeContactsApi#addContact(Account, ContactChange[])
*/
@Override
public ContactChange[] addContact(Account account, ContactChange[] ccList) {
if (account != null) {
// Accounts not supported in 1.X, just return null
return null;
}
/*
* These details need special treatment: Name and Note (written to
* people table); Organization/Title (written as one detail in NAB).
*/
mMarkedOrganizationIndex = mMarkedTitleIndex = -1;
preprocessContactToAdd(ccList);
Uri uri = mCr.insert(People.CONTENT_URI, mValues);
if (uri != null) {
// Change List to hold resulting changes for the add contact
// (including +1 for contact id)
final ContactChange[] idChangeList = new ContactChange[ccList.length + 1];
final long nabContactId = ContentUris.parseId(uri);
idChangeList[0] = ContactChange.createIdsChange(ccList[0],
ContactChange.TYPE_UPDATE_NAB_CONTACT_ID);
idChangeList[0].setNabContactId(nabContactId);
writeDetails(ccList, idChangeList, nabContactId);
processOrganization(ccList, idChangeList, nabContactId);
return idChangeList;
}
return null;
}
/**
* @see NativeContactsApi#updateContact(ContactChange[])
*/
@Override
public ContactChange[] updateContact(ContactChange[] ccList) {
if (ccList == null || ccList.length == 0) {
LogUtils.logW("NativeContactsApi1.updateContact() nothing to update - empty ccList!");
return null;
}
mMarkedOrganizationIndex = mMarkedTitleIndex = -1;
final long nabContactId = ccList[0].getNabContactId();
final int ccListSize = ccList.length;
final ContactChange[] idCcList = new ContactChange[ccListSize];
for (int i = 0; i < ccListSize; i++) {
final ContactChange cc = ccList[i];
if (cc.getKey() == ContactChange.KEY_VCARD_ORG) {
if (mMarkedOrganizationIndex < 0) {
mMarkedOrganizationIndex = i;
}
continue;
}
if (cc.getKey() == ContactChange.KEY_VCARD_TITLE) {
if (mMarkedTitleIndex < 0) {
mMarkedTitleIndex = i;
}
continue;
}
switch (cc.getType()) {
case ContactChange.TYPE_ADD_DETAIL:
final int key = cc.getKey();
if (key != ContactChange.KEY_VCARD_NAME && key != ContactChange.KEY_VCARD_NOTE) {
final long nabDetailId = insertDetail(cc, nabContactId);
if (nabDetailId != ContactChange.INVALID_ID) {
idCcList[i] = ContactChange.createIdsChange(cc,
ContactChange.TYPE_UPDATE_NAB_DETAIL_ID);
idCcList[i].setNabDetailId(nabDetailId);
}
} else {
// Name and Note are not inserted but updated because
// they are always there!
updateDetail(cc);
}
break;
case ContactChange.TYPE_UPDATE_DETAIL:
updateDetail(cc);
break;
case ContactChange.TYPE_DELETE_DETAIL:
deleteDetail(cc.getKey(), nabContactId, cc.getNabDetailId());
break;
default:
break;
}
}
final long orgDetailId = updateOrganization(ccList, nabContactId);
if (orgDetailId != ContactChange.INVALID_ID) {
if (mMarkedOrganizationIndex >= 0
&& ccList[mMarkedOrganizationIndex].getType() == ContactChange.TYPE_ADD_DETAIL) {
idCcList[mMarkedOrganizationIndex] = ContactChange.createIdsChange(
ccList[mMarkedOrganizationIndex], ContactChange.TYPE_UPDATE_NAB_DETAIL_ID);
idCcList[mMarkedOrganizationIndex].setNabDetailId(orgDetailId);
}
if (mMarkedTitleIndex >= 0
&& ccList[mMarkedTitleIndex].getType() == ContactChange.TYPE_ADD_DETAIL) {
idCcList[mMarkedTitleIndex] = ContactChange.createIdsChange(
ccList[mMarkedTitleIndex], ContactChange.TYPE_UPDATE_NAB_DETAIL_ID);
idCcList[mMarkedTitleIndex].setNabDetailId(orgDetailId);
}
}
return idCcList;
}
/**
* @see NativeContactsApi#removeContact(long)
*/
@Override
public void removeContact(long nabContactId) {
Uri rawContactUri = ContentUris.withAppendedId(People.CONTENT_URI, nabContactId);
mCr.delete(rawContactUri, null, null);
}
/**
* @see NativeContactsApi#setSyncAutomatically(boolean)
*/
@Override
public void setSyncAutomatically(boolean syncAutomatically) {
// Nothing to do
}
/**
* @see NativeContactsApi#isKeySupported(int)
*/
@Override
public boolean getMasterSyncAutomatically() {
// Always true in 1.X
return true;
}
/**
* @see NativeContactsApi#setSyncable(boolean)
*/
@Override
public void setSyncable(boolean syncable) {
// Nothing to do
}
/**
* @see NativeContactsApi#isKeySupported(int)
*/
@Override
public boolean isKeySupported(int key) {
return sSupportedKeys.indexOfKey(key) >= 0;
}
/**
* Reads Phone Number details from a Contact into the provided
* {@link ContactChange} List
*
* @param ccList {@link ContactChange} list to add details into
* @param nabContactId ID of the NAB Contact
*/
private void readContactPhoneNumbers(List<ContactChange> ccList, long nabContactId) {
final Uri contactUri = ContentUris.withAppendedId(People.CONTENT_URI, nabContactId);
final Uri phoneUri = Uri.withAppendedPath(contactUri, People.Phones.CONTENT_DIRECTORY);
final Cursor cursor = mCr.query(phoneUri, null, null, null, null);
if (cursor == null) {
return;
}
try {
while (cursor.moveToNext()) {
final ContactChange cc = readContactPhoneNumber(cursor);
if (cc != null) {
cc.setNabContactId(nabContactId);
ccList.add(cc);
}
}
} finally {
CursorUtils.closeCursor(cursor);
}
}
/**
* Reads one Phone Number Detail from the supplied cursor into a
* {@link ContactChange}
*
* @param cursor Cursor to read from
* @return Read Phone Number Detail or null
*/
private ContactChange readContactPhoneNumber(Cursor cursor) {
ContactChange cc = null;
final String phoneNumber = CursorUtils.getString(cursor, Phones.NUMBER);
if (!TextUtils.isEmpty(phoneNumber)) {
final long nabDetailId = CursorUtils.getLong(cursor, Phones._ID);
final boolean isPrimary = CursorUtils.getInt(cursor, Phones.ISPRIMARY) != 0;
final int rawType = CursorUtils.getInt(cursor, Phones.TYPE);
int flags = mapFromNabPhoneType(rawType);
if (isPrimary) {
flags |= ContactChange.FLAG_PREFERRED;
}
cc = new ContactChange(ContactChange.KEY_VCARD_PHONE, phoneNumber, flags);
cc.setNabDetailId(nabDetailId);
}
return cc;
}
/**
* Reads Contact Method details from a Contact into the provided
* {@link ContactChange} List
*
* @param ccList {@link ContactChange} list to add details into
* @param nabContactId ID of the NAB Contact
*/
private void readContactMethods(List<ContactChange> ccList, long nabContactId) {
Uri contactUri = ContentUris.withAppendedId(People.CONTENT_URI, nabContactId);
Uri contactMethodUri = Uri.withAppendedPath(contactUri,
People.ContactMethods.CONTENT_DIRECTORY);
final Cursor cursor = mCr.query(contactMethodUri, null, null, null, null);
if (cursor == null) {
return;
}
try {
while (cursor.moveToNext()) {
final ContactChange cc = readContactMethod(cursor);
if (cc != null) {
cc.setNabContactId(nabContactId);
ccList.add(cc);
}
}
} finally {
CursorUtils.closeCursor(cursor);
}
}
/**
* Reads one Contact Method Detail from the supplied cursor into a
* {@link ContactChange}
*
* @param cursor Cursor to read from
* @return Read Contact Method Detail or null
*/
private ContactChange readContactMethod(Cursor cursor) {
ContactChange cc = null;
final String value = CursorUtils.getString(cursor, ContactMethods.DATA);
if (!TextUtils.isEmpty(value)) {
final long nabDetailId = CursorUtils.getLong(cursor, ContactMethods._ID);
final boolean isPrimary = CursorUtils.getInt(cursor, ContactMethods.ISPRIMARY) != 0;
final int kind = CursorUtils.getInt(cursor, ContactMethods.KIND);
final int type = CursorUtils.getInt(cursor, ContactMethods.TYPE);
int flags = mapFromNabContactMethodType(type);
if (isPrimary) {
flags |= ContactChange.FLAG_PREFERRED;
}
if (kind == CONTACT_METHODS_KIND_EMAIL) {
cc = new ContactChange(ContactChange.KEY_VCARD_EMAIL, value, flags);
cc.setNabDetailId(nabDetailId);
} else if (kind == CONTACT_METHODS_KIND_ADDRESS) {
if (!TextUtils.isEmpty(value)) {
cc = new ContactChange(ContactChange.KEY_VCARD_ADDRESS, VCardHelper
.makePostalAddress(parseRawAddress(value)), flags);
cc.setNabDetailId(nabDetailId);
}
}
}
return cc;
}
/**
* Reads Organization details from a Contact into the provided
* {@link ContactChange} List
*
* @param ccList {@link ContactChange} list to add details into
* @param nabContactId ID of the NAB Contact
*/
private void readContactOrganizations(List<ContactChange> ccList, long nabContactId) {
final Uri contactUri = ContentUris.withAppendedId(People.CONTENT_URI, nabContactId);
final Uri contactOrganizationsUri = Uri.withAppendedPath(contactUri,
Contacts.Organizations.CONTENT_DIRECTORY);
final Cursor cursor = mCr.query(contactOrganizationsUri, null, null, null,
Organizations._ID);
if (cursor == null) {
return;
}
try {
// Only loops while there is not Organization read (CAB limitation!)
while (!mHaveReadOrganization && cursor.moveToNext()) {
readContactOrganization(cursor, ccList, nabContactId);
}
} finally {
CursorUtils.closeCursor(cursor);
}
// Reset the boolean flag
mHaveReadOrganization = false;
}
/**
* Reads one Organization Detail from the supplied cursor into a
* {@link ContactChange} Note that this method may actually read up to 2
* into Contact Changes. However, Organization and Title may only be read if
* a boolean flag is not set
*
* @param cursor Cursor to read from
* @param ccList {@link ContactChange} list to add details to
* @param nabContactId ID of the NAB Contact
* @return Read Contact Method Detail or null
*/
private void readContactOrganization(Cursor cursor, List<ContactChange> ccList,
long nabContactId) {
final long nabDetailId = CursorUtils.getLong(cursor, Organizations._ID);
final boolean isPrimary = CursorUtils.getInt(cursor, Organizations.ISPRIMARY) != 0;
final int type = CursorUtils.getInt(cursor, Organizations.TYPE);
int flags = mapFromNabOrganizationType(type);
if (isPrimary) {
flags |= ContactChange.FLAG_PREFERRED;
}
if (!mHaveReadOrganization) {
// Company
final String company = CursorUtils.getString(cursor, Organizations.COMPANY);
if (!TextUtils.isEmpty(company)) {
// Escaping the value (no need to involve the VCardHelper just
// for the company)
final String escapedCompany = company.replace(";", "\\;");
final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_ORG,
escapedCompany, flags);
cc.setNabContactId(nabContactId);
cc.setNabDetailId(nabDetailId);
ccList.add(cc);
mHaveReadOrganization = true;
}
// Title
final String title = CursorUtils.getString(cursor, Organizations.TITLE);
if (!TextUtils.isEmpty(title)) {
final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_TITLE, title,
flags);
cc.setNabContactId(nabContactId);
cc.setNabDetailId(nabDetailId);
ccList.add(cc);
mHaveReadOrganization = true;
}
}
}
/**
* "Pre-processing" of {@link ContactChange} list before it can be added to
* NAB. This needs to be done because of issues on the 1.X NAB and our CAB
* In 1.X NAB: Name and Note which are stored differently from other details
* In our CAB: Organization and Title are separate details contrary to the
* NAB
*
* @param ccList {@link ContactChange} list for the add operation
*/
private void preprocessContactToAdd(ContactChange[] ccList) {
final int ccListSize = ccList.length;
boolean nameFound = false, noteFound = false;
mValues.clear();
for (int i = 0; i < ccListSize; i++) {
final ContactChange cc = ccList[i];
if (cc != null) {
if (!nameFound && cc.getKey() == ContactChange.KEY_VCARD_NAME) {
final Name name = VCardHelper.getName(cc.getValue());
mValues.put(People.NAME, name.toString());
nameFound = true;
}
if (!noteFound && cc.getKey() == ContactChange.KEY_VCARD_NOTE) {
mValues.put(People.NOTES, cc.getValue());
noteFound = true;
}
if (mMarkedOrganizationIndex < 0 && cc.getKey() == ContactChange.KEY_VCARD_ORG) {
mMarkedOrganizationIndex = i;
}
if (mMarkedTitleIndex < 0 && cc.getKey() == ContactChange.KEY_VCARD_TITLE) {
mMarkedTitleIndex = i;
}
}
}
}
/**
* Writes new details from a provided {@link ContactChange} list into the
* NAB. The resulting detail IDs are put in another provided
* {@link ContactChange} list.
*
* @param ccList {@link ContactChange} list to write from
* @param idChangeList {@link ContatChange} list where IDs for the written
* details are put
* @param nabContactId ID of the NAB Contact
*/
private void writeDetails(ContactChange[] ccList, ContactChange[] idChangeList,
long nabContactId) {
final int ccListSize = ccList.length;
for (int i = 0; i < ccListSize; i++) {
final ContactChange cc = ccList[i];
if (cc != null) {
long nabDetailId = insertDetail(cc, nabContactId);
if (nabDetailId != ContactChange.INVALID_ID) {
// The +1 assumes prior addition of the contact id at
// position 0
idChangeList[i + 1] = ContactChange.createIdsChange(cc,
ContactChange.TYPE_UPDATE_NAB_DETAIL_ID);
idChangeList[i + 1].setNabDetailId(nabDetailId);
}
}
}
}
/**
* Inserts a new detail to NAB
*
* @param cc {@link ContactChange} to read data from
* @param nabContactId ID of the NAB Contact
* @return The created detail's ID
*/
private long insertDetail(ContactChange cc, long nabContactId) {
mValues.clear();
Uri contentUri = null;
switch (cc.getKey()) {
case ContactChange.KEY_VCARD_PHONE:
putPhoneValues(cc, nabContactId);
contentUri = Phones.CONTENT_URI;
break;
case ContactChange.KEY_VCARD_EMAIL:
putContactMethodValues(cc, CONTACT_METHODS_KIND_EMAIL, nabContactId);
contentUri = ContactMethods.CONTENT_URI;
break;
case ContactChange.KEY_VCARD_ADDRESS:
putContactMethodValues(cc, CONTACT_METHODS_KIND_ADDRESS, nabContactId);
contentUri = ContactMethods.CONTENT_URI;
break;
}
if (contentUri != null) {
Uri uri = mCr.insert(contentUri, mValues);
if (uri != null) {
return ContentUris.parseId(uri);
}
}
return ContactChange.INVALID_ID;
}
/**
* Updates a detail.
*
* @param cc {@link ContactChange} to read data from
* @param nabContactId ID of the NAB Contact
* @return true if the detail was updated, false if not
*/
private boolean updateDetail(ContactChange cc) {
mValues.clear();
Uri contentUri = null;
long nabDetailId = cc.getNabDetailId();
final long nabContactId = cc.getNabContactId();
switch (cc.getKey()) {
case ContactChange.KEY_VCARD_PHONE:
putPhoneValues(cc, nabContactId);
contentUri = Phones.CONTENT_URI;
break;
case ContactChange.KEY_VCARD_EMAIL:
putContactMethodValues(cc, CONTACT_METHODS_KIND_EMAIL, nabContactId);
contentUri = ContactMethods.CONTENT_URI;
break;
case ContactChange.KEY_VCARD_ADDRESS:
putContactMethodValues(cc, CONTACT_METHODS_KIND_ADDRESS, nabContactId);
contentUri = ContactMethods.CONTENT_URI;
break;
case ContactChange.KEY_VCARD_NAME:
final Name name = VCardHelper.getName(cc.getValue());
mValues.put(People.NAME, name.toString());
contentUri = People.CONTENT_URI;
nabDetailId = nabContactId;
break;
case ContactChange.KEY_VCARD_NOTE:
mValues.put(People.NOTES, cc.getValue());
contentUri = People.CONTENT_URI;
nabDetailId = nabContactId;
break;
}
if (contentUri != null) {
Uri uri = ContentUris.withAppendedId(contentUri, nabDetailId);
return mCr.update(uri, mValues, null, null) > 0;
}
return false;
}
/**
* Deletes a detail from NAB
*
* @param key The detail key
* @param nabContactId ID of the NAB Contact
* @param nabDetailId ID of the NAB Detail
* @return true if the detail was deleted, false if not
*/
private boolean deleteDetail(int key, long nabContactId, long nabDetailId) {
mValues.clear();
Uri contentUri = null;
switch (key) {
case ContactChange.KEY_VCARD_PHONE:
contentUri = Phones.CONTENT_URI;
break;
case ContactChange.KEY_VCARD_EMAIL:
case ContactChange.KEY_VCARD_ADDRESS:
contentUri = ContactMethods.CONTENT_URI;
break;
case ContactChange.KEY_VCARD_NAME:
mValues.putNull(People.NAME);
contentUri = People.CONTENT_URI;
nabDetailId = nabContactId;
break;
case ContactChange.KEY_VCARD_NOTE:
mValues.putNull(People.NOTES);
contentUri = People.CONTENT_URI;
nabDetailId = nabContactId;
break;
}
if (contentUri != null) {
final Uri uri = ContentUris.withAppendedId(contentUri, nabDetailId);
if (mValues.size() > 0) {
return mCr.update(uri, mValues, null, null) > 0;
} else {
return mCr.delete(uri, null, null) > 0;
}
}
return false;
}
/**
* Put Phone detail into the values
*
* @param cc
* @param nabContactId ID of the NAB Contact
*/
private void putPhoneValues(ContactChange cc, long nabContactId) {
mValues.put(Phones.PERSON_ID, nabContactId);
mValues.put(Phones.NUMBER, cc.getValue());
int flags = cc.getFlags();
mValues.put(Phones.TYPE, mapToNabPhoneType(flags));
mValues.put(Phones.ISPRIMARY, flags & ContactChange.FLAG_PREFERRED);
/*
* Forcing the label field to be null because we don't support custom
* labels and in case we replace from a custom label to home or private
* type without setting label to null, mCr.update() will throw a SQL
* constraint exception.
*/
mValues.put(Phones.LABEL, (String)null);
}
/**
* Put Contact Methods detail into the values
*
* @param cc {@link ContactChange} to read values from
* @param kind The kind of contact method (email or address)
* @param nabContactId ID of the NAB Contact
*/
private void putContactMethodValues(ContactChange cc, int kind, long nabContactId) {
mValues.put(ContactMethods.PERSON_ID, nabContactId);
if (kind == CONTACT_METHODS_KIND_EMAIL) {
mValues.put(ContactMethods.DATA, cc.getValue());
} else {
// Must be Address, once again need to use VCardHelper to extract
// address
PostalAddress address = VCardHelper.getPostalAddress(cc.getValue());
mValues.put(ContactMethods.DATA, address.toString());
}
mValues.put(ContactMethods.KIND, kind);
int flags = cc.getFlags();
mValues.put(ContactMethods.TYPE, mapToNabContactMethodType(flags));
mValues.put(ContactMethods.ISPRIMARY, flags & ContactChange.FLAG_PREFERRED);
}
/**
* Ugly method to process organization for writing to NAB
*
* @param ccList {@link ContactChange} list to fetch organization from
* @param idChangeList {@link ContatChange} list where IDs for the written
* organization are put
* @param nabContactId ID of the NAB Contact
*/
private void processOrganization(ContactChange[] ccList, ContactChange[] idChangeList,
long nabContactId) {
final long organizationId = writeOrganization(ccList, nabContactId);
if (organizationId != ContactChange.INVALID_ID) {
if (mMarkedOrganizationIndex >= 0) {
idChangeList[mMarkedOrganizationIndex + 1] = ContactChange.createIdsChange(
ccList[mMarkedOrganizationIndex], ContactChange.TYPE_UPDATE_NAB_DETAIL_ID);
idChangeList[mMarkedOrganizationIndex + 1].setNabDetailId(organizationId);
}
if (mMarkedTitleIndex >= 0) {
idChangeList[mMarkedTitleIndex + 1] = ContactChange.createIdsChange(
ccList[mMarkedTitleIndex], ContactChange.TYPE_UPDATE_NAB_DETAIL_ID);
idChangeList[mMarkedTitleIndex + 1].setNabDetailId(organizationId);
}
}
}
/**
* Write Organization detail to NAB
*
* @param ccList {@link ContactChange} list where Organization and Title are
* found
* @param nabContactId ID of the NAB Contact
* @return ID of the NAB Contact
*/
private long writeOrganization(ContactChange[] ccList, long nabContactId) {
mValues.clear();
int flags = ContactChange.FLAG_NONE;
if (mMarkedOrganizationIndex >= 0) {
final ContactChange cc = ccList[mMarkedOrganizationIndex];
final Organisation organization = VCardHelper.getOrg(cc.getValue());
if (!TextUtils.isEmpty(organization.name)) {
mValues.put(Organizations.COMPANY, organization.name);
flags |= cc.getFlags();
}
}
if (mMarkedTitleIndex >= 0) {
final ContactChange cc = ccList[mMarkedTitleIndex];
// No need to check for empty values as there is only one
flags |= cc.getFlags();
mValues.put(Organizations.TITLE, cc.getValue());
}
if (mValues.size() > 0) {
mValues.put(Organizations.PERSON_ID, nabContactId);
mValues.put(Organizations.TYPE, mapToNabOrganizationType(flags));
final Uri uri = mCr.insert(Organizations.CONTENT_URI, mValues);
if (uri != null) {
return ContentUris.parseId(uri);
}
}
return ContactChange.INVALID_ID;
}
/**
* Updates the Organization detail in the context of a Contact Update
* operation. The end of result of this is that the Organization may be
* inserted, updated or deleted depending on the update data. For example,
* if the title is deleted but there is also a company name then the
* Organization is just updated. However, if there was no company name then
* the detail should be deleted altogether.
*
* @param ccList {@link ContactChange} list where Organization and Title may
* be found
* @param nabContactId The NAB ID of the Contact
* @return In the Organization insertion case this should contain the new ID
* and in the update case should contain the existing ID
*/
private long updateOrganization(ContactChange[] ccList, long nabContactId) {
if (mMarkedOrganizationIndex < 0 && mMarkedTitleIndex < 0) {
// no organization or title to update - do nothing
return ContactChange.INVALID_ID;
}
long detailId = ContactChange.INVALID_ID;
int flags = ContactChange.FLAG_NONE;
String company = null;
String title = null;
final Uri organizationUri = Uri.withAppendedPath(ContentUris.withAppendedId(
People.CONTENT_URI, nabContactId), Contacts.Organizations.CONTENT_DIRECTORY);
final Cursor cursor = mCr.query(organizationUri, null, null, null, Organizations._ID);
// First retrieve the values that are already present,
// assuming that the lowest id is the one in CAB
try {
if (cursor != null && cursor.moveToNext()) {
company = CursorUtils.getString(cursor, Organizations.COMPANY);
title = CursorUtils.getString(cursor, Organizations.TITLE);
detailId = CursorUtils.getLong(cursor, Organizations._ID);
flags = mapFromNabOrganizationType(CursorUtils.getInt(cursor, Organizations.TYPE));
final boolean isPrimary = CursorUtils.getInt(cursor, Organizations.ISPRIMARY) > 0;
if (isPrimary) {
flags |= ContactChange.FLAG_PREFERRED;
}
}
} finally {
CursorUtils.closeCursor(cursor);
}
if (mMarkedOrganizationIndex >= 0) {
final ContactChange cc = ccList[mMarkedOrganizationIndex];
if (cc.getType() != ContactChange.TYPE_DELETE_DETAIL) {
final String value = cc.getValue();
if (value != null) {
final VCardHelper.Organisation organization = VCardHelper.getOrg(value);
if (!TextUtils.isEmpty(organization.name)) {
company = organization.name;
}
}
flags = cc.getFlags();
} else { // Delete case
company = null;
}
}
if (mMarkedTitleIndex >= 0) {
final ContactChange cc = ccList[mMarkedTitleIndex];
title = cc.getValue();
if (cc.getType() != ContactChange.TYPE_DELETE_DETAIL) {
flags = cc.getFlags();
}
}
if (company != null || title != null) {
mValues.clear();
/*
* Forcing the label field to be null because we don't support
* custom labels and in case we replace from a custom label to home
* or private type without setting label to null, mCr.update() will
* throw a SQL constraint exception.
*/
mValues.put(Organizations.LABEL, (String)null);
mValues.put(Organizations.COMPANY, company);
mValues.put(Organizations.TITLE, title);
mValues.put(Organizations.TYPE, mapToNabOrganizationType(flags));
mValues.put(Organizations.ISPRIMARY, flags & ContactChange.FLAG_PREFERRED);
final Uri existingUri = ContentUris.withAppendedId(Contacts.Organizations.CONTENT_URI,
detailId);
if (detailId != ContactChange.INVALID_ID) {
mCr.update(existingUri, mValues, null, null);
} else {
// insert
final Uri idUri = mCr.insert(organizationUri, mValues);
if (idUri != null) {
return ContentUris.parseId(idUri);
}
}
} else if (detailId != ContactChange.INVALID_ID) {
final Uri existingUri = ContentUris.withAppendedId(Contacts.Organizations.CONTENT_URI,
detailId);
mCr.delete(existingUri, null, null);
} else {
mMarkedOrganizationIndex = mMarkedTitleIndex = -1;
}
// Updated detail id or ContactChange.INVALID_ID if deleted
return detailId;
}
/**
* Maps a phone type from the native value to the {@link ContactChange}
* flags.
*
* @param nabType Given native phone number type
* @return {@link ContactChange} flags
*/
private static int mapFromNabPhoneType(int nabType) {
switch (nabType) {
case Phones.TYPE_HOME:
return ContactChange.FLAG_HOME;
case Phones.TYPE_MOBILE:
return ContactChange.FLAG_CELL;
case Phones.TYPE_WORK:
return ContactChange.FLAG_WORK;
case Phones.TYPE_FAX_HOME:
return ContactChange.FLAGS_HOME_FAX;
case Phones.TYPE_FAX_WORK:
return ContactChange.FLAGS_WORK_FAX;
}
return ContactChange.FLAG_NONE;
}
/**
* Maps {@link ContactChange} flags into the native phone type.
*
* @param flags {@link ContactChange} flags
* @return Native phone type
*/
private static int mapToNabPhoneType(int flags) {
if ((flags & ContactChange.FLAGS_HOME_FAX) == ContactChange.FLAGS_HOME_FAX) {
return Phones.TYPE_FAX_HOME;
}
if ((flags & ContactChange.FLAGS_WORK_FAX) == ContactChange.FLAGS_WORK_FAX) {
return Phones.TYPE_FAX_WORK;
}
if ((flags & ContactChange.FLAG_HOME) == ContactChange.FLAG_HOME) {
return Phones.TYPE_HOME;
}
if ((flags & ContactChange.FLAG_WORK) == ContactChange.FLAG_WORK) {
return Phones.TYPE_WORK;
}
if ((flags & ContactChange.FLAG_CELL) == ContactChange.FLAG_CELL) {
return Phones.TYPE_MOBILE;
}
return Phones.TYPE_OTHER;
}
/**
* Maps a method type from the native value into the {@link ContactChange}
* flags
*
* @param nabType Native method type
* @return {@link ContactChange} flags
*/
private static int mapFromNabContactMethodType(int nabType) {
switch (nabType) {
case ContactMethods.TYPE_HOME:
return ContactChange.FLAG_HOME;
case ContactMethods.TYPE_WORK:
return ContactChange.FLAG_WORK;
}
return ContactChange.FLAG_NONE;
}
/**
* Maps {@link ContactChange} flags into the native method type.
*
* @param flags {@link ContactChange} flags
* @return Native method type
*/
private static int mapToNabContactMethodType(int flags) {
if ((flags & ContactChange.FLAG_HOME) == ContactChange.FLAG_HOME) {
return ContactMethods.TYPE_HOME;
}
if ((flags & ContactChange.FLAG_WORK) == ContactChange.FLAG_WORK) {
return ContactMethods.TYPE_WORK;
}
return ContactMethods.TYPE_OTHER;
}
/**
* Maps a organization type from the native value into the
* {@link ContactChange} flags
*
* @param nabType Given native organization type
* @return {@link ContactChange} flags
*/
private static int mapFromNabOrganizationType(int nabType) {
if (nabType == Organizations.TYPE_WORK) {
return ContactChange.FLAG_WORK;
}
return ContactChange.FLAG_NONE;
}
/**
* Maps {@link ContactChange} flags into the native organization type.
*
* @param flags {@link ContactChange} flags
* @return Native Organization type
*/
private static int mapToNabOrganizationType(int flags) {
if ((flags & ContactChange.FLAG_WORK) == ContactChange.FLAG_WORK) {
return Organizations.TYPE_WORK;
}
return Organizations.TYPE_OTHER;
}
/**
* Utility method used to parse a raw display Address into a
* {@link VCardHelper.PostalAddress} object. The purpose of this method is
* simply to remove '\n' separators. It does not aim to correctly map
* address components to the right place, e.g. it is not guaranteed that
* 'PostalAddress.Country' will really be the Country.
*/
private static PostalAddress parseRawAddress(String rawAddress) {
final PostalAddress address = new PostalAddress();
final String[] tokens = rawAddress.trim().split("\n");
final int numTokens = tokens.length;
address.addressLine1 = tokens[0];
if (numTokens > 1) {
address.addressLine2 = tokens[1];
}
if (numTokens > 2) {
address.city = tokens[2];
}
if (numTokens > 3) {
address.county = tokens[3];
}
if (numTokens > 4) {
address.postCode = tokens[4];
}
if (numTokens > 5) {
address.country = tokens[5];
}
if (numTokens > 6) {
final StringBuilder sb = new StringBuilder();
sb.append(tokens[6]);
for (int i = 7; i < numTokens; i++) {
sb.append(' ');
sb.append(tokens[i]);
}
address.postOfficeBox = sb.toString();
}
return address;
}
}