/* * 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.HashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.accounts.AccountManager; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.OperationApplicationException; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.provider.ContactsContract; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.Groups; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.Settings; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Event; 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.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; import android.text.TextUtils; import android.util.SparseArray; import com.vodafone360.people.R; 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.service.SyncAdapter; import com.vodafone360.people.utils.CursorUtils; import com.vodafone360.people.utils.LogUtils; import com.vodafone360.people.utils.VersionUtils; /** * The implementation of the NativeContactsApi for the Android 2.X platform. */ public class NativeContactsApi2 extends NativeContactsApi { /** * Convenience Projection to fetch only a Raw Contact's ID and Native * Account Type */ private static final String[] CONTACTID_PROJECTION = new String[] { RawContacts._ID, RawContacts.ACCOUNT_TYPE }; /** * Raw ID Column for the CONTACTID_PROJECTION Projection */ private static final int CONTACTID_PROJECTION_RAW_ID = 0; /** * Account Type Column for the CONTACTID_PROJECTION Projection */ private static final int CONTACTID_PROJECTION_ACCOUNT_TYPE = 1; /** * Group ID Projection */ private static final String[] GROUPID_PROJECTION = new String[] { Groups._ID }; /** * Vendor specific account. Only used in 2.x API. */ private static Account sPhoneAccount = null; /** * Regular expression for a date that can be handled by the People Client at * present. Matches the following cases: N-n-n n-n-N Where: - 'n' * corresponds to one or two digits - 'N' corresponds to two or 4 digits */ public static final String COMPLETE_DATE_REGEX = "(\\d{1,4}-\\d{1,2}-\\d{1,2}|\\d{1,2}-\\d{1,2}-\\d{1,4})"; /** * 'My Contacts' System group where clause */ private static final String MY_CONTACTS_GROUP_WHERE_CLAUSE = Groups.SYSTEM_ID + "=\"Contacts\""; /** * Selection where clause for a NULL Account */ private static final String NULL_ACCOUNT_WHERE_CLAUSE = RawContacts.ACCOUNT_NAME + " IS NULL AND " + RawContacts.ACCOUNT_TYPE + " IS NULL"; /** * Selection where clause for an Organization detail. */ private static final String ORGANIZATION_DETAIL_WHERE_CLAUSE = RawContacts.Data.MIMETYPE + "=\"" + Organization.CONTENT_ITEM_TYPE + "\""; /** * The list of Uri that need to be listened to for contacts changes on * native side. */ private static final Uri[] sUri = { ContactsContract.RawContacts.CONTENT_URI, ContactsContract.Data.CONTENT_URI }; /** * Holds mapping from a NAB type (MIME) to a VCard Key */ private static final HashMap<String, Integer> sFromNabContentTypeToKeyMap; /** * Holds mapping from a VCard Key to a NAB type (MIME) */ private static final SparseArray<String> sFromKeyToNabContentTypeArray; static { sFromNabContentTypeToKeyMap = new HashMap<String, Integer>(9, 1); sFromNabContentTypeToKeyMap.put(StructuredName.CONTENT_ITEM_TYPE, ContactChange.KEY_VCARD_NAME); sFromNabContentTypeToKeyMap.put(Nickname.CONTENT_ITEM_TYPE, ContactChange.KEY_VCARD_NICKNAME); sFromNabContentTypeToKeyMap.put(Phone.CONTENT_ITEM_TYPE, ContactChange.KEY_VCARD_PHONE); sFromNabContentTypeToKeyMap.put(Email.CONTENT_ITEM_TYPE, ContactChange.KEY_VCARD_EMAIL); sFromNabContentTypeToKeyMap.put(StructuredPostal.CONTENT_ITEM_TYPE, ContactChange.KEY_VCARD_ADDRESS); sFromNabContentTypeToKeyMap.put(Organization.CONTENT_ITEM_TYPE, ContactChange.KEY_VCARD_ORG); sFromNabContentTypeToKeyMap.put(Website.CONTENT_ITEM_TYPE, ContactChange.KEY_VCARD_URL); sFromNabContentTypeToKeyMap.put(Note.CONTENT_ITEM_TYPE, ContactChange.KEY_VCARD_NOTE); sFromNabContentTypeToKeyMap.put(Event.CONTENT_ITEM_TYPE, ContactChange.KEY_VCARD_DATE); // sFromNabContentTypeToKeyMap.put(Photo.CONTENT_ITEM_TYPE, ContactChange.KEY_PHOTO); sFromKeyToNabContentTypeArray = new SparseArray<String>(10); sFromKeyToNabContentTypeArray.append(ContactChange.KEY_VCARD_NAME, StructuredName.CONTENT_ITEM_TYPE); sFromKeyToNabContentTypeArray.append(ContactChange.KEY_VCARD_NICKNAME, Nickname.CONTENT_ITEM_TYPE); sFromKeyToNabContentTypeArray.append(ContactChange.KEY_VCARD_PHONE, Phone.CONTENT_ITEM_TYPE); sFromKeyToNabContentTypeArray.append(ContactChange.KEY_VCARD_EMAIL, Email.CONTENT_ITEM_TYPE); sFromKeyToNabContentTypeArray.append(ContactChange.KEY_VCARD_ADDRESS, StructuredPostal.CONTENT_ITEM_TYPE); sFromKeyToNabContentTypeArray.append(ContactChange.KEY_VCARD_ORG, Organization.CONTENT_ITEM_TYPE); // Special case: VCARD_TITLE maps to the same NAB type as sFromKeyToNabContentTypeArray.append(ContactChange.KEY_VCARD_TITLE, Organization.CONTENT_ITEM_TYPE); sFromKeyToNabContentTypeArray.append(ContactChange.KEY_VCARD_URL, Website.CONTENT_ITEM_TYPE); sFromKeyToNabContentTypeArray.append(ContactChange.KEY_VCARD_NOTE, Note.CONTENT_ITEM_TYPE); sFromKeyToNabContentTypeArray.append(ContactChange.KEY_VCARD_DATE, Event.CONTENT_ITEM_TYPE); // sFromKeyToNabContentTypeArray.append(ContactChange.KEY_PHOTO, Photo.CONTENT_ITEM_TYPE); } /** * The observer registered by the upper layer. */ private ContactsObserver mAbstractedObserver; /** * Content values used for writing to NAB. */ private final ContentValues mValues = new ContentValues(); /** * The content observers that listens for native contacts changes. */ private final ContentObserver[] mContentObservers = new ContentObserver[sUri.length]; /** * Array of row ID in the groups table to the "My Contacts" System Group. * The reason for this being an array is because there may be multiple * "My Contacts" groups (Platform Bug!?). */ private long[] mMyContactsGroupRowIds = null; /** * Arguably another example where Organization and Title force us into extra * effort. We use this value to pass the correct detail ID when an 'add * detail' is done for one the two although the other is already present. * Make sure to reset this value for every UpdateContact operation */ private long mExistingOrganizationId = ContactChange.INVALID_ID; /** * Flag to check if we have already read a Birthday detail */ private boolean mHaveReadBirthday = false; /** * Yield value for our ContentProviderOperations. */ private boolean mYield = true; /** * Batch used for Contact Writing operations. */ private BatchOperation mBatch = new BatchOperation(); /** * Inner class for applying batches. TODO: Move to own class if batches * become supported in other areas */ private class BatchOperation { // List for storing the batch mOperations ArrayList<ContentProviderOperation> mOperations; /** * Default constructor */ public BatchOperation() { mOperations = new ArrayList<ContentProviderOperation>(); } /** * Current size of the batch * * @return Size of the batch */ public int size() { return mOperations.size(); } /** * Adds a new operation to the batch * * @param cpo The */ public void add(ContentProviderOperation cpo) { mOperations.add(cpo); } /** * Clears all operations in the batch Effectively resets the batch. */ public void clear() { mOperations.clear(); } public ContentProviderResult[] execute() { ContentProviderResult[] resultArray = null; if (mOperations.size() > 0) { // Apply the mOperations to the content provider try { resultArray = mCr.applyBatch(ContactsContract.AUTHORITY, mOperations); } catch (final OperationApplicationException e1) { LogUtils.logE("storing contact data failed", e1); } catch (final RemoteException e2) { LogUtils.logE("storing contact data failed", e2); } mOperations.clear(); } return resultArray; } } /** * Convenience interface to map the generic DATA column names to the People * profile detail column names. */ private interface PeopleProfileColumns { /** * 360 People profile MIME Type */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/com.vodafone360.people.profile"; /** * 360 People Contact's profile ID (Corresponds to Contact's Internal * Contact ID) */ public static final String DATA_PID = Data.DATA1; /** * 360 People Contact profile Summary */ public static final String DATA_SUMMARY = Data.DATA2; /** * 360 People Contact profile detail */ public static final String DATA_DETAIL = Data.DATA3; } /** * This class holds a native content observer that will be notified in case * of changes on the registered URI. */ private class NativeContentObserver extends ContentObserver { public NativeContentObserver() { super(new Handler()); } @Override public void onChange(boolean selfChange) { LogUtils.logI("NativeContactsApi2.NativeContentObserver.onChange(" + selfChange + ")"); mAbstractedObserver.onChange(); } } /** * @see NativeContactsApi#registerObserver(ContactsObserver) */ @Override public void registerObserver(ContactsObserver observer) { LogUtils.logI("NativeContactsApi2.registerObserver()"); if (mAbstractedObserver != null) { throw new RuntimeException( "Only one observer at a time supported... Please unregister first."); } mAbstractedObserver = observer; final ContentResolver cr = mContext.getContentResolver(); for (int i = 0; i < mContentObservers.length; i++) { mContentObservers[i] = new NativeContentObserver(); cr.registerContentObserver(sUri[i], true, mContentObservers[i]); } } /** * @see NativeContactsApi#unregisterObserver(ContactsObserver) */ @Override public void unregisterObserver() { LogUtils.logI("NativeContactsApi2.unregisterObserver()"); if (mCr != null) { mAbstractedObserver = null; for (int i = 0; i < mContentObservers.length; i++) { if (mContentObservers[i] != null) { mCr.unregisterContentObserver(mContentObservers[i]); mContentObservers[i] = null; } } } } /** * @see NativeContactsApi#initialize() */ @Override protected void initialize() { // perform here any one time initialization sPhoneAccount = getVendorSpecificAccount(); } /** * @see NativeContactsApi#getAccounts() */ @Override public Account[] getAccounts() { AccountManager accountManager = AccountManager.get(mContext); final android.accounts.Account[] accounts2xApi = accountManager.getAccounts(); Account[] accounts = null; if (accounts2xApi.length > 0) { accounts = new Account[accounts2xApi.length]; for (int i = 0; i < accounts2xApi.length; i++) { accounts[i] = new Account(accounts2xApi[i].name, accounts2xApi[i].type); } } return accounts; } /** * Reads vendor specific accounts from settings and through accountmanager. * Some phones with custom ui like sense have additional account that we * have to take care of. This method tryes to read them * * @return Account object if vendor specific account is found, null * otherwise */ public Account getVendorSpecificAccount() { // first read the settings String[] PROJECTION = { Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE, Settings.UNGROUPED_VISIBLE }; // Get a cursor with all people Cursor cursor = mCr.query(Settings.CONTENT_URI, PROJECTION, null, null, null); // Got no cursor? Return with null! if (null == cursor) { return null; } try { String[] values = new String[cursor.getCount()]; for (int i = 0; i < values.length; i++) { cursor.moveToNext(); if (isVendorSpecificAccount(cursor.getString(1))) { return new Account(cursor.getString(0), cursor.getString(1)); } } } catch (Exception exc) { return null; } CursorUtils.closeCursor(cursor); // nothing found in the settings? try accountmanager Account[] accounts = getAccounts(); if (accounts == null) { return null; } for (Account account : accounts) { if (isVendorSpecificAccount(account.getType())) { return account; } } return null; } /** * @see NativeContactsApi2#getAccountsByType(String) */ @Override public Account[] getAccountsByType(int type) { switch (type) { // people and google type lead to the same block the difference is // then handled inside in two if statements. // Otherwise we would have a lot of redundant code, case PEOPLE_ACCOUNT_TYPE: case GOOGLE_ACCOUNT_TYPE: { AccountManager accountMan = AccountManager.get(mContext); android.accounts.Account[] accounts2xApi = null; // For people account set the people account type string... if (PEOPLE_ACCOUNT_TYPE == type) { accounts2xApi = accountMan.getAccountsByType(PEOPLE_ACCOUNT_TYPE_STRING); } // .. and for google the same only with google string. if (GOOGLE_ACCOUNT_TYPE == type) { accounts2xApi = accountMan.getAccountsByType(GOOGLE_ACCOUNT_TYPE_STRING); } final int numAccounts = accounts2xApi.length; if (numAccounts > 0) { Account[] accounts = new Account[numAccounts]; for (int i = 0; i < numAccounts; i++) { accounts[i] = new Account(accounts2xApi[i].name, accounts2xApi[i].type); } return accounts; } else { return null; } } case PHONE_ACCOUNT_TYPE: { if (sPhoneAccount == null) { return null; } return new Account[] { sPhoneAccount }; } default: return null; } } /** * @see NativeContactsApi#addPeopleAccount(String) */ @Override public boolean addPeopleAccount(String username) { boolean isAdded = false; try { android.accounts.Account account = new android.accounts.Account(username, PEOPLE_ACCOUNT_TYPE_STRING); AccountManager accountMan = AccountManager.get(mContext); isAdded = accountMan.addAccountExplicitly(account, null, null); if (isAdded) { if (VersionUtils.isHtcSenseDevice(mContext)) { createSettingsEntryForAccount(username); requestSyncAdapterInitialization(account); } // Need to do our Sync Adapter initialization logic here SyncAdapter.initialize(account, ContactsContract.AUTHORITY); } } catch (Exception ex) { LogUtils.logE("People Account creation failed because of exception:\n", ex); } // Commented out 'My Contacts' Group Row IDs call as it is not currently used // if (isAdded) { // // Updating MyContacts Group IDs here for now because it needs to be // // done one time just before first time sync. // // In the future, this code may change if we modify // // the way we retrieve contact IDs. // fetchMyContactsGroupRowIds(); // } return isAdded; } /** * @see NativeContactsApi#isPeopleAccountCreated() */ @Override public boolean isPeopleAccountCreated() { return getPeopleAccount() != null; } /** * @see NativeContactsApi#removePeopleAccount() */ @Override public void removePeopleAccount() { AccountManager accountMan = AccountManager.get(mContext); android.accounts.Account[] accounts = accountMan .getAccountsByType(PEOPLE_ACCOUNT_TYPE_STRING); if (accounts != null && accounts.length > 0) { accountMan.removeAccount(accounts[0], null, null); } } /** * @see NativeContactsApi#getContactIds(Account) */ @Override public long[] getContactIds(Account account) { // Need to construct a where clause if (account != null) { final StringBuffer clauseBuffer = new StringBuffer(); clauseBuffer.append(RawContacts.ACCOUNT_NAME); clauseBuffer.append("=\""); clauseBuffer.append(account.getName()); clauseBuffer.append("\" AND "); clauseBuffer.append(RawContacts.ACCOUNT_TYPE); clauseBuffer.append("=\""); clauseBuffer.append(account.getType()); clauseBuffer.append('\"'); return getContactIds(clauseBuffer.toString()); } else { return getContactIds(NULL_ACCOUNT_WHERE_CLAUSE); } } /** * @see NativeContactsApi#getContact(long) */ @Override public ContactChange[] getContact(long nabContactId) { // Reset the boolean flags mHaveReadOrganization = false; mHaveReadBirthday = false; final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, nabContactId); final Uri dataUri = Uri.withAppendedPath(rawContactUri, RawContacts.Data.CONTENT_DIRECTORY); final Cursor cursor = mCr.query(dataUri, null, null, null, null); try { if (cursor != null && cursor.getCount() > 0) { final List<ContactChange> ccList = new ArrayList<ContactChange>(); while (cursor.moveToNext()) { readDetail(cursor, ccList, nabContactId); } return ccList.toArray(new ContactChange[ccList.size()]); } } finally { CursorUtils.closeCursor(cursor); } return null; } /** * @see NativeContactsApi#addContact(Account, ContactChange[]) */ @Override public ContactChange[] addContact(Account account, ContactChange[] ccList) { // Make sure to reset all the member variables we need mYield = true; mMarkedOrganizationIndex = mMarkedTitleIndex = -1; mValues.clear(); mBatch.clear(); mValues.put(RawContacts.ACCOUNT_TYPE, account.getType()); mValues.put(RawContacts.ACCOUNT_NAME, account.getName()); ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert( addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI)).withYieldAllowed(mYield) .withValues(mValues); mBatch.add(builder.build()); // according to the calling method ccList here has at least 1 element, ccList[0] is not null final int ccListSize = ccList.length; for (int i = 0; i < ccListSize; i++) { final ContactChange cc = ccList[i]; if (cc != null) { final int key = cc.getKey(); if (key == ContactChange.KEY_VCARD_ORG && mMarkedOrganizationIndex < 0) { // Mark for later writing mMarkedOrganizationIndex = i; continue; } if (key == ContactChange.KEY_VCARD_TITLE && mMarkedTitleIndex < 0) { // Mark for later writing mMarkedTitleIndex = i; continue; } putDetail(cc); addValuesToBatch(ContactChange.INVALID_ID); } } // Special case for the organization/title if it was flagged in the // putDetail call putOrganization(ccList); addValuesToBatch(ContactChange.INVALID_ID); // Add 360 profile detail addProfileAction(ccList[0].getInternalContactId()); // Execute the batch and Generate ID changes from it return executeNewContactBatch(ccList); } /** * @see NativeContactsApi#updateContact(ContactChange[]) */ @Override public ContactChange[] updateContact(ContactChange[] ccList) { if (ccList == null || ccList.length == 0) { LogUtils.logW("NativeContactsApi2.updateContact() nothing to update - empty ccList!"); return null; } // Make sure to reset all the member variables we need mYield = true; mBatch.clear(); mValues.clear(); mMarkedOrganizationIndex = mMarkedTitleIndex = -1; mExistingOrganizationId = ContactChange.INVALID_ID; final long nabContactId = ccList[0].getNabContactId(); if (nabContactId == ContactChange.INVALID_ID) { LogUtils .logW("NativeContactsApi2.updateContact() Ignoring update request because of invalid NAB Contact ID. Internal Contact ID is " + ccList[0].getInternalContactId()); return null; } final int ccListSize = ccList.length; for (int i = 0; i < ccListSize; i++) { final ContactChange cc = ccList[i]; final int key = cc.getKey(); if (key == ContactChange.KEY_VCARD_ORG && mMarkedOrganizationIndex < 0) { // Mark for later writing mMarkedOrganizationIndex = i; continue; } if (key == ContactChange.KEY_VCARD_TITLE && mMarkedTitleIndex < 0) { // Mark for later writing mMarkedTitleIndex = i; continue; } switch (cc.getType()) { case ContactChange.TYPE_ADD_DETAIL: putDetail(cc); addValuesToBatch(nabContactId); // not a new contact break; case ContactChange.TYPE_UPDATE_DETAIL: if (cc.getNabDetailId() != ContactChange.INVALID_ID) { putDetail(cc); addUpdateValuesToBatch(cc.getNabDetailId()); } else { LogUtils .logW("NativeContactsApi2.updateContact() Ignoring update to detail for " + cc.getKeyToString() + " because of invalid NAB Detail ID. Internal Contact is " + cc.getInternalContactId() + ", Internal Detail Id is " + cc.getInternalDetailId()); } break; case ContactChange.TYPE_DELETE_DETAIL: if (cc.getNabDetailId() != ContactChange.INVALID_ID) { addDeleteDetailToBatch(cc.getNabDetailId()); } else { LogUtils .logW("NativeContactsApi2.updateContact() Ignoring detail deletion for " + cc.getKeyToString() + " because of invalid NAB Detail ID. Internal Contact is " + cc.getInternalContactId() + ", Internal Detail Id is " + cc.getInternalDetailId()); } break; default: break; } } updateOrganization(ccList, nabContactId); // Execute the batch and Generate ID changes from it return executeUpdateContactBatch(ccList); } /** * @see NativeContactsApi#removeContact(long) */ @Override public void removeContact(long nabContactId) { mCr.delete(addCallerIsSyncAdapterParameter(ContentUris.withAppendedId( RawContacts.CONTENT_URI, nabContactId)), null, null); } /** * @see NativeContactsApi#getMasterSyncAutomatically */ @Override public boolean getMasterSyncAutomatically() { return ContentResolver.getMasterSyncAutomatically(); } /** * @see NativeContactsApi#setSyncable(boolean) */ @Override public void setSyncable(boolean syncable) { android.accounts.Account account = getPeopleAccount(); if(account != null) { ContentResolver. setIsSyncable(account, ContactsContract.AUTHORITY, syncable ? 1 : 0); } } /** * @see NativeContactsApi#setSyncAutomatically(boolean) */ @Override public void setSyncAutomatically(boolean syncAutomatically) { android.accounts.Account account = getPeopleAccount(); if(account != null) { ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, syncAutomatically); if(syncAutomatically) { // Kick start sync ContentResolver.requestSync(account, ContactsContract.AUTHORITY, new Bundle()); } else { // Cancel ongoing just in case ContentResolver.cancelSync(account, ContactsContract.AUTHORITY); } } } /** * @see NativeContactsApi#isKeySupported(int) */ @Override public boolean isKeySupported(int key) { // Only supported if in the SparseArray return sFromKeyToNabContentTypeArray.indexOfKey(key) >= 0; } /** * Inner (private) method for getting contact ids. Allows a cleaner * separation the public API method. * * @param selection The where clause for the operation * @return An array containing the Contact IDs that have been found */ private long[] getContactIds(final String selection) { // Store ids in a temporary array because of possible null values long[] tempIds = null; int idCount = 0; final Cursor cursor = mCr.query(RawContacts.CONTENT_URI, CONTACTID_PROJECTION, selection, null, null); if (cursor == null) { return null; } try { final int cursorCount = cursor.getCount(); if (cursorCount > 0) { tempIds = new long[cursorCount]; while (cursor.moveToNext()) { final long id = cursor.getLong(CONTACTID_PROJECTION_RAW_ID); final String accountType = cursor.getString(CONTACTID_PROJECTION_ACCOUNT_TYPE); // TODO: Remove hardcoding (if statement) if (TextUtils.isEmpty(accountType) || accountType.equals(NativeContactsApi.PEOPLE_ACCOUNT_TYPE_STRING) || isVendorSpecificAccount(accountType)) { //PAND-2125 || isContactInMyContactsGroup(id)) { tempIds[idCount] = id; idCount++; } } } } finally { CursorUtils.closeCursor(cursor); } if (idCount > 0) { // Here we copy the array to strip any eventual nulls at the end of // the tempIds array final long[] ids = new long[idCount]; System.arraycopy(tempIds, 0, ids, 0, idCount); return ids; } return null; } /** * Fetches the IDs corresponding to the "My Contacts" System Group The * reason why we return an array is because there may be multiple * "My Contacts" groups. * * @return Array containing all IDs of the "My Contacts" group or null if * none exist */ private void fetchMyContactsGroupRowIds() { final Cursor cursor = mCr.query(Groups.CONTENT_URI, GROUPID_PROJECTION, MY_CONTACTS_GROUP_WHERE_CLAUSE, null, null); if (cursor == null) { return; } try { final int count = cursor.getCount(); if (count > 0) { mMyContactsGroupRowIds = new long[count]; for (int i = 0; i < count; i++) { cursor.moveToNext(); mMyContactsGroupRowIds[i] = cursor.getLong(0); } } } finally { CursorUtils.closeCursor(cursor); } } /** * Checks if a Contact is in the "My Contacts" System Group * * @param nabContactId ID of the Contact to check * @return true if the Contact is in the Group, false if not */ /** * PAND-2125 * The decision has been made to stop the import of Google contacts on 2.x devices. * From now on, we will only import native addressbook contacts. */ /* private boolean isContactInMyContactsGroup(long nabContactId) { boolean belongs = false; if (mMyContactsGroupRowIds != null) { final Uri dataUri = Uri.withAppendedPath(ContentUris.withAppendedId( RawContacts.CONTENT_URI, nabContactId), RawContacts.Data.CONTENT_DIRECTORY); // build query containing row ID values final StringBuilder sb = new StringBuilder(); sb.append(MY_CONTACTS_MULTI_GROUP_MEMBERSHIP); final int rowIdCount = mMyContactsGroupRowIds.length; for (int i = 0; i < rowIdCount; i++) { sb.append(mMyContactsGroupRowIds[i]); if (i < rowIdCount - 1) { sb.append(','); } } sb.append(')'); final Cursor cursor = mCr.query(dataUri, null, sb.toString(), null, null); try { belongs = cursor != null && cursor.getCount() > 0; } finally { CursorUtils.closeCursor(cursor); } } return belongs; } */ /** * Reads a Contact Detail from a Cursor into the supplied Contact Change * List. * * @param cursor Cursor to read from * @param ccList List of Contact Changes to read Detail into * @param nabContactId NAB ID of the Contact */ private void readDetail(Cursor cursor, List<ContactChange> ccList, long nabContactId) { final String mimetype = CursorUtils.getString(cursor, Data.MIMETYPE); final Integer key = sFromNabContentTypeToKeyMap.get(mimetype); if (key != null) { switch (key.intValue()) { case ContactChange.KEY_VCARD_NAME: readName(cursor, ccList, nabContactId); break; case ContactChange.KEY_VCARD_NICKNAME: readNickname(cursor, ccList, nabContactId); break; case ContactChange.KEY_VCARD_PHONE: readPhone(cursor, ccList, nabContactId); break; case ContactChange.KEY_VCARD_EMAIL: readEmail(cursor, ccList, nabContactId); break; case ContactChange.KEY_VCARD_ADDRESS: readAddress(cursor, ccList, nabContactId); break; case ContactChange.KEY_VCARD_ORG: // Only one Organization can be read (CAB limitation!) if (!mHaveReadOrganization) { readOrganization(cursor, ccList, nabContactId); } break; case ContactChange.KEY_VCARD_URL: readWebsite(cursor, ccList, nabContactId); break; case ContactChange.KEY_VCARD_DATE: if (!mHaveReadBirthday) { readBirthday(cursor, ccList, nabContactId); } break; default: // The default case is also a valid key final String value = CursorUtils.getString(cursor, Data.DATA1); if (!TextUtils.isEmpty(value)) { final long nabDetailId = CursorUtils.getLong(cursor, Data._ID); final ContactChange cc = new ContactChange(key, value, ContactChange.FLAG_NONE); cc.setNabContactId(nabContactId); cc.setNabDetailId(nabDetailId); ccList.add(cc); } break; } } } /** * Reads an name detail as a {@link ContactChange} from the provided cursor. * For this type of detail we need to use a VCARD (semicolon separated) * value. * * @param cursor Cursor to read from * @param ccList List of Contact Changes to add read detail data * @param nabContactId ID of the NAB Contact */ private void readName(Cursor cursor, List<ContactChange> ccList, long nabContactId) { // Using display name only to check if there is a valid name to read final String displayName = CursorUtils.getString(cursor, StructuredName.DISPLAY_NAME); if (!TextUtils.isEmpty(displayName)) { final long nabDetailId = CursorUtils.getLong(cursor, StructuredName._ID); // VCard Helper data type (CAB) final Name name = new Name(); // NAB: Given name -> CAB: First name name.firstname = CursorUtils.getString(cursor, StructuredName.GIVEN_NAME); // NAB: Family name -> CAB: Surname name.surname = CursorUtils.getString(cursor, StructuredName.FAMILY_NAME); // NAB: Prefix -> CAB: Title name.title = CursorUtils.getString(cursor, StructuredName.PREFIX); // NAB: Middle name -> CAB: Middle name name.midname = CursorUtils.getString(cursor, StructuredName.MIDDLE_NAME); // NAB: Suffix -> CAB: Suffixes name.suffixes = CursorUtils.getString(cursor, StructuredName.SUFFIX); // NOTE: Ignoring Phonetics (DATA7, DATA8 and DATA9)! // TODO: Need to get middle name and concatenate into value final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_NAME, VCardHelper .makeName(name), ContactChange.FLAG_NONE); cc.setNabContactId(nabContactId); cc.setNabDetailId(nabDetailId); ccList.add(cc); } } /** * Reads an nickname detail as a {@link ContactChange} from the provided * cursor. * * @param cursor Cursor to read from * @param ccList List of Contact Changes to add read detail data * @param nabContactId ID of the NAB Contact */ private void readNickname(Cursor cursor, List<ContactChange> ccList, long nabContactId) { final String value = CursorUtils.getString(cursor, Nickname.NAME); if (!TextUtils.isEmpty(value)) { final long nabDetailId = CursorUtils.getLong(cursor, Nickname._ID); /* * TODO: Decide what to do with nickname: Can only have one in VF360 * but NAB Allows more than one! */ final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_NICKNAME, value, ContactChange.FLAG_NONE); cc.setNabContactId(nabContactId); cc.setNabDetailId(nabDetailId); ccList.add(cc); } } /** * Reads an phone detail as a {@link ContactChange} from the provided * cursor. * * @param cursor Cursor to read from * @param ccList List of Contact Changes to add read detail data * @param nabContactId ID of the NAB Contact */ private void readPhone(Cursor cursor, List<ContactChange> ccList, long nabContactId) { final String value = CursorUtils.getString(cursor, Phone.NUMBER); if (!TextUtils.isEmpty(value)) { final long nabDetailId = CursorUtils.getLong(cursor, Phone._ID); final int type = CursorUtils.getInt(cursor, Phone.TYPE); int flags = mapFromNabPhoneType(type); final boolean isPrimary = CursorUtils.getInt(cursor, Phone.IS_PRIMARY) != 0; if (isPrimary) { flags |= ContactChange.FLAG_PREFERRED; } // assuming raw value is good enough for us final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_PHONE, value, flags); cc.setNabContactId(nabContactId); cc.setNabDetailId(nabDetailId); ccList.add(cc); } } /** * Reads an email detail as a {@link ContactChange} from the provided * cursor. * * @param cursor Cursor to read from * @param ccList List of Contact Changes to add read detail data * @param nabContactId ID of the NAB Contact */ private void readEmail(Cursor cursor, List<ContactChange> ccList, long nabContactId) { final String value = CursorUtils.getString(cursor, Email.DATA); if (!TextUtils.isEmpty(value)) { final long nabDetailId = CursorUtils.getLong(cursor, Email._ID); final int type = CursorUtils.getInt(cursor, Email.TYPE); int flags = mapFromNabEmailType(type); final boolean isPrimary = CursorUtils.getInt(cursor, Email.IS_PRIMARY) != 0; if (isPrimary) { flags |= ContactChange.FLAG_PREFERRED; } // assuming raw value is good enough for us final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_EMAIL, value, flags); cc.setNabContactId(nabContactId); cc.setNabDetailId(nabDetailId); ccList.add(cc); } } /** * Reads an address detail as a {@link ContactChange} from the provided * cursor. For this type of detail we need to use a VCARD (semicolon * separated) value. * * @param cursor Cursor to read from * @param ccList List of Contact Changes to add read detail data * @param nabContactId ID of the NAB Contact */ private void readAddress(Cursor cursor, List<ContactChange> ccList, long nabContactId) { // Using formatted address only to check if there is a valid address to // read final String formattedAddress = CursorUtils.getString(cursor, StructuredPostal.FORMATTED_ADDRESS); if (!TextUtils.isEmpty(formattedAddress)) { final long nabDetailId = CursorUtils.getLong(cursor, StructuredPostal._ID); final int type = CursorUtils.getInt(cursor, StructuredPostal.TYPE); int flags = mapFromNabAddressType(type); final boolean isPrimary = CursorUtils.getInt(cursor, StructuredPostal.IS_PRIMARY) != 0; if (isPrimary) { flags |= ContactChange.FLAG_PREFERRED; } // VCard Helper data type (CAB) final PostalAddress address = new PostalAddress(); // NAB: Street -> CAB: AddressLine1 address.addressLine1 = CursorUtils.getString(cursor, StructuredPostal.STREET); // NAB: PO Box -> CAB: postOfficeBox address.postOfficeBox = CursorUtils.getString(cursor, StructuredPostal.POBOX); // NAB: Neighborhood -> CAB: AddressLine2 address.addressLine2 = CursorUtils.getString(cursor, StructuredPostal.NEIGHBORHOOD); // NAB: City -> CAB: City address.city = CursorUtils.getString(cursor, StructuredPostal.CITY); // NAB: Region -> CAB: County address.county = CursorUtils.getString(cursor, StructuredPostal.REGION); // NAB: Post code -> CAB: Post code address.postCode = CursorUtils.getString(cursor, StructuredPostal.POSTCODE); // NAB: Country -> CAB: Country address.country = CursorUtils.getString(cursor, StructuredPostal.COUNTRY); final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_ADDRESS, VCardHelper .makePostalAddress(address), flags); cc.setNabContactId(nabContactId); cc.setNabDetailId(nabDetailId); ccList.add(cc); } } /** * Reads an organization detail as a {@link ContactChange} from the provided * cursor. For this type of detail we need to use a VCARD (semicolon * separated) value. In reality two different changes may be read if a title * is also present. * * @param cursor Cursor to read from * @param ccList List of Contact Changes to add read detail data * @param nabContactId ID of the NAB Contact */ private void readOrganization(Cursor cursor, List<ContactChange> ccList, long nabContactId) { final int type = CursorUtils.getInt(cursor, Organization.TYPE); int flags = mapFromNabOrganizationType(type); final boolean isPrimary = CursorUtils.getInt(cursor, Organization.IS_PRIMARY) != 0; if (isPrimary) { flags |= ContactChange.FLAG_PREFERRED; } final long nabDetailId = CursorUtils.getLong(cursor, Organization._ID); if (!mHaveReadOrganization) { // VCard Helper data type (CAB) final Organisation organization = new Organisation(); // Company organization.name = CursorUtils.getString(cursor, Organization.COMPANY); // Department final String department = CursorUtils.getString(cursor, Organization.DEPARTMENT); if (!TextUtils.isEmpty(department)) { organization.unitNames.add(department); } if ((organization.unitNames != null && organization.unitNames.size() > 0) || !TextUtils.isEmpty(organization.name)) { final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_ORG, VCardHelper .makeOrg(organization), flags); cc.setNabContactId(nabContactId); cc.setNabDetailId(nabDetailId); ccList.add(cc); mHaveReadOrganization = true; } // Title final String title = CursorUtils.getString(cursor, Organization.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; } } } /** * Reads an Website detail as a {@link ContactChange} from the provided * cursor. * * @param cursor Cursor to read from * @param ccList List of Contact Changes to add read detail data * @param nabContactId ID of the NAB Contact */ private void readWebsite(Cursor cursor, List<ContactChange> ccList, long nabContactId) { final String url = CursorUtils.getString(cursor, Website.URL); if (!TextUtils.isEmpty(url)) { final long nabDetailId = CursorUtils.getLong(cursor, Website._ID); final int type = CursorUtils.getInt(cursor, Website.TYPE); int flags = mapFromNabWebsiteType(type); final boolean isPrimary = CursorUtils.getInt(cursor, Website.IS_PRIMARY) != 0; if (isPrimary) { flags |= ContactChange.FLAG_PREFERRED; } final ContactChange cc = new ContactChange(ContactChange.KEY_VCARD_URL, url, flags); cc.setNabContactId(nabContactId); cc.setNabDetailId(nabDetailId); ccList.add(cc); } } /** * Reads an Birthday detail as a {@link ContactChange} from the provided * cursor. Note that since the Android Type is the Generic "Event", it may * be the case that nothing is read if this is not actually a Birthday * * @param cursor Cursor to read from * @param ccList List of Contact Changes to add read detail data * @param nabContactId ID of the NAB Contact */ private void readBirthday(Cursor cursor, List<ContactChange> ccList, long nabContactId) { final int type = CursorUtils.getInt(cursor, Event.TYPE); if (type != Event.TYPE_BIRTHDAY) { // Not a Birthday, return return; } final String date = CursorUtils.getString(cursor, Event.START_DATE); if(TextUtils.isEmpty(date)) { // Empty date, do nothing return; } // Ignoring birthdays without year, day and month! // FIXME: Remove this check when/if the backend becomes able to // handle incomplete birthdays Matcher matcher = Pattern.compile(NativeContactsApi2.COMPLETE_DATE_REGEX).matcher(date); if(!matcher.find()) { // No matching date, log and return LogUtils.logD("NativeContactsApi2.readBirthday() - Unsupported '"+ date+"' Date skipped for NAB Contact ID:"+nabContactId); return; } final long nabDetailId = CursorUtils.getLong(cursor, Event._ID); final ContactChange cc = new ContactChange( ContactChange.KEY_VCARD_DATE, matcher.group(), ContactChange.FLAG_BIRTHDAY); cc.setNabContactId(nabContactId); cc.setNabDetailId(nabDetailId); ccList.add(cc); mHaveReadBirthday = true; } /** * Adds current values to the batch. * * @param nabContactId The existing NAB Contact ID if it is an update or an * invalid id if a new contact */ private void addValuesToBatch(long nabContactId) { // Add to batch if (mValues.size() > 0) { final boolean isNewContact = nabContactId == ContactChange.INVALID_ID; if (!isNewContact) { // Updating a Contact, need to add the ID to the Values mValues.put(Data.RAW_CONTACT_ID, nabContactId); } ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert( addCallerIsSyncAdapterParameter(Data.CONTENT_URI)).withYieldAllowed(mYield) .withValues(mValues); if (isNewContact) { // New Contact needs Back Reference builder.withValueBackReference(Data.RAW_CONTACT_ID, 0); } mYield = false; mBatch.add(builder.build()); } } /** * Adds current update values to the batch. * * @param nabDetailId The NAB ID of the detail to update */ private void addUpdateValuesToBatch(long nabDetailId) { if (mValues.size() > 0) { final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, nabDetailId); ContentProviderOperation.Builder builder = ContentProviderOperation.newUpdate( addCallerIsSyncAdapterParameter(uri)).withYieldAllowed(mYield).withValues( mValues); mYield = false; mBatch.add(builder.build()); } } /** * Adds a delete detail operation to the batch. * * @param nabDetailId The NAB Id of the detail to delete */ private void addDeleteDetailToBatch(long nabDetailId) { final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, nabDetailId); ContentProviderOperation.Builder builder = ContentProviderOperation.newDelete( addCallerIsSyncAdapterParameter(uri)).withYieldAllowed(mYield); mYield = false; mBatch.add(builder.build()); } /** * Adds the profile action detail to a (assumed) pending new Contact Batch * operation. * * @param internalContactId The Internal Contact ID used as the Profile ID */ private void addProfileAction(long internalContactId) { mValues.clear(); mValues.put(Data.MIMETYPE, PeopleProfileColumns.CONTENT_ITEM_TYPE); mValues.put(PeopleProfileColumns.DATA_PID, internalContactId); mValues.put(PeopleProfileColumns.DATA_SUMMARY, mContext .getString(R.string.android_contact_profile_summary)); mValues.put(PeopleProfileColumns.DATA_DETAIL, mContext .getString(R.string.android_contact_profile_detail)); addValuesToBatch(ContactChange.INVALID_ID); } // PRESENCE TEXT NOT USED // /** // * Returns the Data id for a sample SyncAdapter contact's profile row, or // 0 // * if the sample SyncAdapter user isn't found. // * // * @param resolver a content resolver // * @param userId the sample SyncAdapter user ID to lookup // * @return the profile Data row id, or 0 if not found // */ // private long lookupProfile(long internalContactId) { // long profileId = -1; // final Cursor c = // mCr.query(Data.CONTENT_URI, ProfileQuery.PROJECTION, // ProfileQuery.SELECTION, new String[] {String.valueOf(internalContactId)}, // null); // try { // if (c != null && c.moveToFirst()) { // profileId = c.getLong(ProfileQuery.COLUMN_ID); // } // } finally { // if (c != null) { // c.close(); // } // } // return profileId; // } // // /** // * Constants for a query to find a contact given a sample SyncAdapter user // * ID. // */ // private interface ProfileQuery { // public final static String[] PROJECTION = new String[] {Data._ID}; // // public final static int COLUMN_ID = 0; // // public static final String SELECTION = // Data.MIMETYPE + "='" + PeopleProfileColumns.CONTENT_ITEM_TYPE // + "' AND " + PeopleProfileColumns.DATA_PID + "=?"; // } // // private void addPresence(long internalContactId, String presenceText) { // long profileId = lookupProfile(internalContactId); // if(profileId > -1) { // mValues.clear(); // mValues.put(StatusUpdates.DATA_ID, profileId); // mValues.put(StatusUpdates.STATUS, presenceText); // mValues.put(StatusUpdates.PROTOCOL, Im.PROTOCOL_CUSTOM); // mValues.put(StatusUpdates.IM_ACCOUNT, LoginPreferences.getUsername()); // mValues.put(StatusUpdates.STATUS_ICON, R.drawable.pt_launcher_icon); // mValues.put(StatusUpdates.STATUS_RES_PACKAGE, mContext.getPackageName()); // // ContentProviderOperation.Builder builder = // ContentProviderOperation.newInsert( // addCallerIsSyncAdapterParameter(StatusUpdates.CONTENT_URI)). // withYieldAllowed(mYield).withValues(mValues); // BatchOperation batch = new BatchOperation(); // batch.add(builder.build()); // batch.execute(); // } // } /** * Put values for a detail from a {@link ContactChange} into pending values. * * @param cc {@link ContactChange} to read values from */ private void putDetail(ContactChange cc) { mValues.clear(); switch (cc.getKey()) { case ContactChange.KEY_VCARD_PHONE: putPhone(cc); break; case ContactChange.KEY_VCARD_EMAIL: putEmail(cc); break; case ContactChange.KEY_VCARD_NAME: putName(cc); break; case ContactChange.KEY_VCARD_NICKNAME: putNickname(cc); break; case ContactChange.KEY_VCARD_ADDRESS: putAddress(cc); break; case ContactChange.KEY_VCARD_URL: putWebsite(cc); break; case ContactChange.KEY_VCARD_NOTE: putNote(cc); break; case ContactChange.KEY_VCARD_DATE: // Date only means Birthday currently putBirthday(cc); break; default: break; } } /** * Put Name detail into the values * * @param cc {@link ContactChange} to read values from */ private void putName(ContactChange cc) { final Name name = VCardHelper.getName(cc.getValue()); if (name == null) { // Nothing to do return; } mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); mValues.put(StructuredName.GIVEN_NAME, name.firstname); mValues.put(StructuredName.FAMILY_NAME, name.surname); mValues.put(StructuredName.PREFIX, name.title); mValues.put(StructuredName.MIDDLE_NAME, name.midname); mValues.put(StructuredName.SUFFIX, name.suffixes); } /** * Put Nickname detail into the values * * @param cc {@link ContactChange} to read values from */ private void putNickname(ContactChange cc) { mValues.put(Nickname.NAME, cc.getValue()); mValues.put(Nickname.MIMETYPE, Nickname.CONTENT_ITEM_TYPE); } /** * Put Phone detail into the values * * @param cc {@link ContactChange} to read values from */ private void putPhone(ContactChange cc) { mValues.put(Phone.NUMBER, cc.getValue()); final int flags = cc.getFlags(); mValues.put(Phone.TYPE, mapToNabPhoneType(flags)); mValues.put(Phone.IS_PRIMARY, flags & ContactChange.FLAG_PREFERRED); mValues.put(Phone.MIMETYPE, Phone.CONTENT_ITEM_TYPE); } /** * Put Email detail into the values * * @param cc {@link ContactChange} to read values from */ private void putEmail(ContactChange cc) { mValues.put(Email.DATA, cc.getValue()); final int flags = cc.getFlags(); mValues.put(Email.TYPE, mapToNabEmailType(flags)); mValues.put(Email.IS_PRIMARY, flags & ContactChange.FLAG_PREFERRED); mValues.put(Email.MIMETYPE, Email.CONTENT_ITEM_TYPE); } /** * Put Address detail into the values * * @param cc {@link ContactChange} to read values from */ private void putAddress(ContactChange cc) { final PostalAddress address = VCardHelper.getPostalAddress(cc.getValue()); if (address == null) { // Nothing to do return; } mValues.put(StructuredPostal.STREET, address.addressLine1); mValues.put(StructuredPostal.POBOX, address.postOfficeBox); mValues.put(StructuredPostal.NEIGHBORHOOD, address.addressLine2); mValues.put(StructuredPostal.CITY, address.city); mValues.put(StructuredPostal.REGION, address.county); mValues.put(StructuredPostal.POSTCODE, address.postCode); mValues.put(StructuredPostal.COUNTRY, address.country); final int flags = cc.getFlags(); mValues.put(StructuredPostal.TYPE, mapToNabAddressType(flags)); mValues.put(StructuredPostal.IS_PRIMARY, flags & ContactChange.FLAG_PREFERRED); mValues.put(StructuredPostal.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE); } /** * Put Website detail into the values * * @param cc {@link ContactChange} to read values from */ private void putWebsite(ContactChange cc) { mValues.put(Website.URL, cc.getValue()); final int flags = cc.getFlags(); mValues.put(Website.TYPE, mapToNabWebsiteType(flags)); mValues.put(Website.IS_PRIMARY, flags & ContactChange.FLAG_PREFERRED); mValues.put(Website.MIMETYPE, Website.CONTENT_ITEM_TYPE); } /** * Put Note detail into the values * * @param cc {@link ContactChange} to read values from */ private void putNote(ContactChange cc) { mValues.put(Note.NOTE, cc.getValue()); mValues.put(Note.MIMETYPE, Note.CONTENT_ITEM_TYPE); } /** * Put Birthday detail into the values * * @param cc {@link ContactChange} to read values from */ private void putBirthday(ContactChange cc) { if ((cc.getFlags() & ContactChange.FLAG_BIRTHDAY) == ContactChange.FLAG_BIRTHDAY) { mValues.put(Event.START_DATE, cc.getValue()); mValues.put(Event.TYPE, Event.TYPE_BIRTHDAY); mValues.put(Event.MIMETYPE, Event.CONTENT_ITEM_TYPE); } } // PHOTO NOT USED // /** // * Do a GET request and retrieve up to maxBytes bytes // * // * @param url // * @param maxBytes // * @return // * @throws IOException // */ // public static byte[] doGetAndReturnBytes(URL url, int maxBytes) throws // IOException { // HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // conn.setRequestMethod("GET"); // InputStream istr = null; // try { // int rc = conn.getResponseCode(); // if (rc != 200) { // throw new IOException("code " + rc + " '" + conn.getResponseMessage() + // "'"); // } // istr = new BufferedInputStream(conn.getInputStream(), 512); // ByteArrayOutputStream baos = new ByteArrayOutputStream(); // copy(istr, baos, maxBytes); // return baos.toByteArray(); // } finally { // if (istr != null) { // istr.close(); // } // } // } // // /** // * Copy maxBytes from an input stream to an output stream. // * @param in // * @param out // * @param maxBytes // * @return // * @throws IOException // */ // private static int copy(InputStream in, OutputStream out, int maxBytes) // throws IOException { // byte[] buf = new byte[512]; // int bytesRead = 1; // int totalBytes = 0; // while (bytesRead > 0) { // bytesRead = in.read(buf, 0, Math.min(512, maxBytes - totalBytes)); // if (bytesRead > 0) { // out.write(buf, 0, bytesRead); // totalBytes += bytesRead; // } // } // return totalBytes; // } // // /** // * Put Photo detail into the values // * @param cc {@link ContactChange} to read values from // */ // private void putPhoto(ContactChange cc) { // try { // // File file = new File(cc.getValue()); // // InputStream is = new FileInputStream(file); // // byte[] bytes = new byte[(int) file.length()]; // // is.read(bytes); // // is.close(); // final URL url = new URL(cc.getValue()); // byte[] bytes = doGetAndReturnBytes(url, 1024 * 100); // mValues.put(Photo.PHOTO, bytes); // mValues.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE); // } catch(Exception ex) { // LogUtils.logE("Unable to put Photo detail because of exception:"+ex); // } // } /** * Put Organization detail into the values * * @param cc {@link ContactChange} to read values from */ private void putOrganization(ContactChange[] ccList) { mValues.clear(); int flags = ContactChange.FLAG_NONE; if (mMarkedOrganizationIndex > -1) { final ContactChange cc = ccList[mMarkedOrganizationIndex]; flags |= cc.getFlags(); final Organisation organization = VCardHelper.getOrg(cc.getValue()); if (organization != null) { mValues.put(Organization.COMPANY, organization.name); if (organization.unitNames.size() > 0) { // Only considering one unit name (department) as that's all // we support mValues.put(Organization.DEPARTMENT, organization.unitNames.get(0)); } else { mValues.putNull(Organization.DEPARTMENT); } } } if (mMarkedTitleIndex > -1) { final ContactChange cc = ccList[mMarkedTitleIndex]; flags |= cc.getFlags(); // No need to check for empty values as there is only one mValues.put(Organization.TITLE, cc.getValue()); } if (mValues.size() > 0) { mValues.put(Organization.LABEL, (String)null); mValues.put(Organization.TYPE, mapToNabOrganizationType(flags)); mValues.put(Organization.IS_PRIMARY, flags & ContactChange.FLAG_PREFERRED); mValues.put(Organization.MIMETYPE, Organization.CONTENT_ITEM_TYPE); } } /** * 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 */ private void updateOrganization(ContactChange[] ccList, long nabContactId) { if (mMarkedOrganizationIndex < 0 && mMarkedTitleIndex < 0) { // no organization or title to update - do nothing return; } // First we check if there is an existing Organization detail in NAB final Uri uri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, nabContactId), RawContacts.Data.CONTENT_DIRECTORY); Cursor cursor = mCr.query(uri, null, ORGANIZATION_DETAIL_WHERE_CLAUSE, null, RawContacts.Data._ID); String company = null; String department = null; String title = null; int flags = ContactChange.FLAG_NONE; try { if (cursor != null && cursor.moveToNext()) { // Found an organization detail company = CursorUtils.getString(cursor, Organization.COMPANY); department = CursorUtils.getString(cursor, Organization.DEPARTMENT); title = CursorUtils.getString(cursor, Organization.TITLE); flags = mapFromNabOrganizationType(CursorUtils.getInt(cursor, Organization.TYPE)); final boolean isPrimary = CursorUtils.getInt(cursor, Organization.IS_PRIMARY) > 0; if (isPrimary) { flags |= ContactChange.FLAG_PREFERRED; } mExistingOrganizationId = CursorUtils.getLong(cursor, Organization._ID); } } finally { CursorUtils.closeCursor(cursor); cursor = null; // make it a candidate for the GC } if (mMarkedOrganizationIndex >= 0) { // Have an Organization (Company + Department) to update final ContactChange cc = ccList[mMarkedOrganizationIndex]; if (cc.getType() != ContactChange.TYPE_DELETE_DETAIL) { final String value = cc.getValue(); if (value != null) { final Organisation organization = VCardHelper.getOrg(value); company = organization.name; if (organization.unitNames.size() > 0) { department = organization.unitNames.get(0); } } flags = cc.getFlags(); } else { // Delete case company = null; department = null; } } if (mMarkedTitleIndex >= 0) { // Have a Title to update final ContactChange cc = ccList[mMarkedTitleIndex]; title = cc.getValue(); if (cc.getType() != ContactChange.TYPE_UPDATE_DETAIL) { flags = cc.getFlags(); } } if (company != null || department != null || title != null) { /* * If any of the above are present we assume a insert or update is * needed. */ mValues.clear(); mValues.put(Organization.LABEL, (String)null); mValues.put(Organization.COMPANY, company); mValues.put(Organization.DEPARTMENT, department); mValues.put(Organization.TITLE, title); mValues.put(Organization.TYPE, mapToNabOrganizationType(flags)); mValues.put(Organization.IS_PRIMARY, flags & ContactChange.FLAG_PREFERRED); mValues.put(Organization.MIMETYPE, Organization.CONTENT_ITEM_TYPE); if (mExistingOrganizationId != ContactChange.INVALID_ID) { // update is needed addUpdateValuesToBatch(mExistingOrganizationId); } else { // insert is needed addValuesToBatch(nabContactId); // not a new contact } } else if (mExistingOrganizationId != ContactChange.INVALID_ID) { /* * Had an Organization but now all values are null, delete is in * order. */ addDeleteDetailToBatch(mExistingOrganizationId); } } /** * Executes a pending Batch Operation related to adding a new Contact. * * @param ccList The original {@link ContactChange} for the new Contact * @return {@link ContactChange} array containing new IDs (may contain some * null elements) */ private ContactChange[] executeNewContactBatch(ContactChange[] ccList) { if (mBatch.size() == 0) { // Nothing to execute LogUtils.logW("NativeContactsApi2.executeNewContactBatch() - the batch is empty, probably none of the changes are supported"); return null; } final ContentProviderResult[] results = mBatch.execute(); if (results == null || results.length == 0) { LogUtils.logE("NativeContactsApi2.executeNewContactBatch()" + "Batch execution result is null or empty!"); return null; } // -1 because we skip the Profile detail final int resultsSize = results.length - 1; if (results[0].uri == null) { // Contact was not created LogUtils.logE("NativeContactsApi2.executeNewContactBatch()" + "NAB Contact ID not found for created contact"); return null; } final long nabContactId = ContentUris.parseId(results[0].uri); final ContactChange[] idChangeList = new ContactChange[ccList.length + 1]; // Update NAB Contact ID CC idChangeList[0] = ContactChange.createIdsChange(ccList[0], ContactChange.TYPE_UPDATE_NAB_CONTACT_ID); idChangeList[0].setNabContactId(nabContactId); // Start after contact id in the results index int resultsIndex = 1, ccListIndex = 0; final boolean haveOrganization = mMarkedOrganizationIndex != -1 || mMarkedTitleIndex != -1; while (resultsIndex < resultsSize) { if (ccListIndex == mMarkedOrganizationIndex || ccListIndex == mMarkedTitleIndex) { ccListIndex++; continue; } if (results[resultsIndex].uri == null) { throw new RuntimeException("NativeContactsApi2.executeNewContactBatch()" + "Unexpected null URI for NAB Contact:" + nabContactId); } if (resultsIndex == resultsSize - 1 && haveOrganization) { // for readability we leave Organization/Title for outside the // loop break; } final ContactChange cc = ccList[ccListIndex]; // Check if the key is one that is supported in the 2.X NAB if (sFromKeyToNabContentTypeArray.indexOfKey(cc.getKey()) >= 0) { final long nabDetailId = ContentUris.parseId(results[resultsIndex].uri); final ContactChange idChange = ContactChange.createIdsChange(ccList[ccListIndex], ContactChange.TYPE_UPDATE_NAB_DETAIL_ID); idChange.setNabContactId(nabContactId); idChange.setNabDetailId(nabDetailId); idChangeList[ccListIndex + 1] = idChange; resultsIndex++; } ccListIndex++; } if (haveOrganization) { final long nabDetailId = ContentUris.parseId(results[resultsIndex].uri); if (mMarkedOrganizationIndex > -1) { final ContactChange idChange = ContactChange.createIdsChange( ccList[mMarkedOrganizationIndex], ContactChange.TYPE_UPDATE_NAB_DETAIL_ID); idChange.setNabContactId(nabContactId); idChange.setNabDetailId(nabDetailId); idChangeList[mMarkedOrganizationIndex + 1] = idChange; } if (mMarkedTitleIndex > -1) { final ContactChange idChange = ContactChange.createIdsChange( ccList[mMarkedTitleIndex], ContactChange.TYPE_UPDATE_NAB_DETAIL_ID); idChange.setNabContactId(nabContactId); idChange.setNabDetailId(nabDetailId); idChangeList[mMarkedTitleIndex + 1] = idChange; } } return idChangeList; } /** * Executes a pending Batch Operation related to updating a Contact. * * @param ccList The original {@link ContactChange} for the Contact update * @return {@link ContactChange} array containing new IDs (may contain some * null elements) */ private ContactChange[] executeUpdateContactBatch(ContactChange[] ccList) { if (mBatch.size() == 0) { // Nothing to execute LogUtils.logW("NativeContactsApi2.executeUpdateContactBatch() - the batch is empty, probably none of the changes are supported"); return null; } final ContentProviderResult[] results = mBatch.execute(); final int resultsSize = results.length; if (results == null || resultsSize == 0) { // Assuming this can happen in case of no added details LogUtils.logE("NativeContactsApi2.executeUpdateContactBatch()" + "Batch execution result is null or empty!"); return null; } // Start after contact id in the results index int resultsIndex = 0, ccListIndex = 0; final boolean haveOrganization = mMarkedOrganizationIndex != -1 || mMarkedTitleIndex != -1; final ContactChange[] idChangeList = new ContactChange[ccList.length]; while (resultsIndex < resultsSize) { if (ccListIndex == mMarkedOrganizationIndex || ccListIndex == mMarkedTitleIndex) { ccListIndex++; continue; } if (results[resultsIndex].uri == null) { // Assuming detail was updated or deleted (not added) resultsIndex++; continue; } if (resultsIndex == resultsSize - 1 && haveOrganization) { // for readability we leave Organization/Title for outside the // loop break; } final ContactChange cc = ccList[ccListIndex]; // Check if the key is one that is supported in the 2.X NAB if (sFromKeyToNabContentTypeArray.indexOfKey(cc.getKey()) >= 0 && cc.getType() == ContactChange.TYPE_ADD_DETAIL) { final long nabDetailId = ContentUris.parseId(results[resultsIndex].uri); final ContactChange idChange = ContactChange.createIdsChange(ccList[ccListIndex], ContactChange.TYPE_UPDATE_NAB_DETAIL_ID); idChange.setNabDetailId(nabDetailId); idChangeList[ccListIndex] = idChange; resultsIndex++; } ccListIndex++; } if (haveOrganization) { long nabDetailId = ContactChange.INVALID_ID; if (mExistingOrganizationId != nabDetailId) { nabDetailId = mExistingOrganizationId; } else if (results[resultsIndex].uri != null) { nabDetailId = ContentUris.parseId(results[resultsIndex].uri); } else { throw new RuntimeException("NativeContactsApi2.executeUpdateContactBatch()" + "Unexpected null Organization URI for NAB Contact:" + ccList[0].getNabContactId()); } if (mMarkedOrganizationIndex > -1 && ccList[mMarkedOrganizationIndex].getType() == ContactChange.TYPE_ADD_DETAIL) { final ContactChange idChange = ContactChange.createIdsChange( ccList[mMarkedOrganizationIndex], ContactChange.TYPE_UPDATE_NAB_DETAIL_ID); idChange.setNabDetailId(nabDetailId); idChangeList[mMarkedOrganizationIndex] = idChange; } if (mMarkedTitleIndex > -1 && ccList[mMarkedTitleIndex].getType() == ContactChange.TYPE_ADD_DETAIL) { final ContactChange idChange = ContactChange.createIdsChange( ccList[mMarkedTitleIndex], ContactChange.TYPE_UPDATE_NAB_DETAIL_ID); idChange.setNabDetailId(nabDetailId); idChangeList[mMarkedTitleIndex] = idChange; } } return idChangeList; } /** * Static utility method that adds the Sync Adapter flag to the provided URI * * @param uri URI to add the flag to * @return URI with the flag */ private static Uri addCallerIsSyncAdapterParameter(Uri uri) { return uri.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") .build(); } /** * Utility method used to create an entry in the ContactsContract.Settings * database table for the 360 People Account. This method only exists to * compensate for HTC devices such as Legend or Desire that don't create the * entry automatically like the Nexus One. * * @param username The username of the 360 People Account. */ private void createSettingsEntryForAccount(String username) { mValues.clear(); mValues.put(Settings.ACCOUNT_NAME, username); mValues.put(Settings.ACCOUNT_TYPE, PEOPLE_ACCOUNT_TYPE_STRING); mValues.put(Settings.UNGROUPED_VISIBLE, true); mValues.put(Settings.SHOULD_SYNC, false); // TODO Unsupported for now mCr.insert(Settings.CONTENT_URI, mValues); } /** * Requests the SyncAdapter to perform a sync with initialization sequence. * * @param account the account to be initialized */ private void requestSyncAdapterInitialization(android.accounts.Account account) { final Bundle bundle = new Bundle(); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); ContentResolver.requestSync(account, ContactsContract.AUTHORITY, bundle); } /** * 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 Phone.TYPE_HOME: return ContactChange.FLAG_HOME; case Phone.TYPE_MOBILE: return ContactChange.FLAG_CELL; case Phone.TYPE_WORK: return ContactChange.FLAG_WORK; case Phone.TYPE_WORK_MOBILE: return ContactChange.FLAGS_WORK_CELL; case Phone.TYPE_FAX_WORK: return ContactChange.FLAGS_WORK_FAX; case Phone.TYPE_FAX_HOME: return ContactChange.FLAGS_HOME_FAX; case Phone.TYPE_OTHER_FAX: return ContactChange.FLAG_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_WORK_CELL) == ContactChange.FLAGS_WORK_CELL) { return Phone.TYPE_WORK_MOBILE; } if ((flags & ContactChange.FLAGS_HOME_FAX) == ContactChange.FLAGS_HOME_FAX) { return Phone.TYPE_FAX_HOME; } if ((flags & ContactChange.FLAGS_WORK_FAX) == ContactChange.FLAGS_WORK_FAX) { return Phone.TYPE_FAX_WORK; } if ((flags & ContactChange.FLAG_HOME) == ContactChange.FLAG_HOME) { return Phone.TYPE_HOME; } if ((flags & ContactChange.FLAG_WORK) == ContactChange.FLAG_WORK) { return Phone.TYPE_WORK; } if ((flags & ContactChange.FLAG_CELL) == ContactChange.FLAG_CELL) { return Phone.TYPE_MOBILE; } if ((flags & ContactChange.FLAG_FAX) == ContactChange.FLAG_FAX) { return Phone.TYPE_OTHER_FAX; } return Phone.TYPE_OTHER; } /** * Maps a email type from the native value into the {@link ContactChange} * flags * * @param nabType Native email type * @return {@link ContactChange} flags */ private static int mapFromNabEmailType(int nabType) { switch (nabType) { case Email.TYPE_HOME: return ContactChange.FLAG_HOME; case Email.TYPE_MOBILE: return ContactChange.FLAG_CELL; case Email.TYPE_WORK: return ContactChange.FLAG_WORK; } return ContactChange.FLAG_NONE; } /** * Maps {@link ContactChange} flags into the native email type. * * @param flags {@link ContactChange} flags * @return Native email type */ private static int mapToNabEmailType(int flags) { if ((flags & ContactChange.FLAG_HOME) == ContactChange.FLAG_HOME) { return Email.TYPE_HOME; } if ((flags & ContactChange.FLAG_WORK) == ContactChange.FLAG_WORK) { return Email.TYPE_WORK; } return Email.TYPE_OTHER; } /** * Maps a address type from the native value into the {@link ContactChange} * flags * * @param nabType Native email type * @return {@link ContactChange} flags */ private static int mapFromNabAddressType(int nabType) { switch (nabType) { case StructuredPostal.TYPE_HOME: return ContactChange.FLAG_HOME; case StructuredPostal.TYPE_WORK: return ContactChange.FLAG_WORK; } return ContactChange.FLAG_NONE; } /** * Maps {@link ContactChange} flags into the native address type. * * @param flags {@link ContactChange} flags * @return Native address type */ private static int mapToNabAddressType(int flags) { if ((flags & ContactChange.FLAG_HOME) == ContactChange.FLAG_HOME) { return StructuredPostal.TYPE_HOME; } if ((flags & ContactChange.FLAG_WORK) == ContactChange.FLAG_WORK) { return StructuredPostal.TYPE_WORK; } return StructuredPostal.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 == Organization.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 Organization.TYPE_WORK; } return Organization.TYPE_OTHER; } /** * Maps a Website type from the native value into the {@link ContactChange} * flags * * @param nabType Native email type * @return {@link ContactChange} flags */ private static int mapFromNabWebsiteType(int nabType) { switch (nabType) { case Website.TYPE_HOME: return ContactChange.FLAG_HOME; case Website.TYPE_WORK: return ContactChange.FLAG_WORK; } return ContactChange.FLAG_NONE; } /** * Maps {@link ContactChange} flags into the native Website type. * * @param flags {@link ContactChange} flags * @return Native Website type */ private static int mapToNabWebsiteType(int flags) { if ((flags & ContactChange.FLAG_HOME) == ContactChange.FLAG_HOME) { return Website.TYPE_HOME; } if ((flags & ContactChange.FLAG_WORK) == ContactChange.FLAG_WORK) { return Website.TYPE_WORK; } return Website.TYPE_OTHER; } /** * Gets the first People Account found on the device or * null if none is found. * @return The Android People account found or null */ private android.accounts.Account getPeopleAccount() { android.accounts.Account[] accounts = AccountManager.get(mContext).getAccountsByType(PEOPLE_ACCOUNT_TYPE_STRING); if(accounts != null && accounts.length > 0) { return accounts[0]; } return null; } }