/* * 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; } }