/* * 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.Arrays; import java.util.Comparator; import android.text.TextUtils; import com.vodafone360.people.datatypes.VCardHelper; import com.vodafone360.people.engine.contactsync.NativeContactsApi.Account; import com.vodafone360.people.utils.DynamicArrayLong; import com.vodafone360.people.utils.LogUtils; import com.vodafone360.people.utils.VersionUtils; /** * The NativeImporter class is responsible for importing contacts from the * native address book to the people database. To do so, it compares the native * address book and the people database to find new changes and update the * people database accordingly. Usage: - instantiate the class - call the tick() * method until isDone() returns true - current progress can be obtained via * getPosition() / getCount() - check the getResult() to get a status (OK, * KO...) */ public class NativeImporter { /** * The undefined result when the NativeImporter has not been run yet. * * @see #getResult() */ public final static int RESULT_UNDEFINED = -1; /** * The ok result when the NativeImporter has finished successfully. * * @see #getResult() */ public final static int RESULT_OK = 0; /** * The undefined result when the NativeImporter has not been run yet. * * @see #getResult() */ public final static int RESULT_ERROR = 1; /** * Number of contacts to be processed "per tick". This is the start value * and is adapted to get as close as possible to TARGET_TIME_PER_TICK * * @see #tick() */ private final static float CONTACTS_PER_TICK_START = 2.0F; /** * Ideal processing time per tick in ms */ private final static float TARGET_TIME_PER_TICK = 200.0F; /** * Contacts to process in one tick. This float will be rounded. */ private float mContactsPerTick = CONTACTS_PER_TICK_START; /** * Handler to the People Contacts API. */ private PeopleContactsApi mPeopleContactsApi; /** * Handler to the Native Contacts API. */ private NativeContactsApi mNativeContactsApi; /** * Internal state representing the task to perform: gets the list of native * contacts ids and people contacts ids. */ private final static int STATE_GET_IDS_LISTS = 0; /** * Internal state representing the task to perform: iterates through the * list of ids to find differences (i.e. added contact, modified contact, * deleted contact). */ private final static int STATE_ITERATE_THROUGH_IDS = 1; /** * Internal state representing the task to perform: process the list of * deleted contacts (i.e. delete the contacts from the people database). * TODO: remove this state */ private final static int STATE_PROCESS_DELETED = 2; /** * Internal state representing the task to perform: final state, nothing * else to perform. */ private final static int STATE_DONE = 3; /** * The current state. */ private int mState = STATE_GET_IDS_LISTS; /** * The list of native ids from the native side. */ private long[] mNativeContactsIds; /** * The list of native ids from the people side. */ private long[] mPeopleNativeContactsIds; /** * The total count of ids to process (native database + people database). */ private int mTotalIds = 0; /** * The current count of processed ids. */ private int mProcessedIds = 0; /** * The index of the current native id. * * @see #mNativeContactsIds */ private int mCurrentNativeIndex = 0; /** * The index in the current people id. * * @see #mPeopleNativeContactsIds */ private int mCurrentPeopleIndex = 0; /** * The index of the current deleted people id. * * @see #mDeletedIds */ private int mCurrentDeletedIndex = 0; /** * Array to store the people ids of contacts to delete. */ private DynamicArrayLong mDeletedIds = new DynamicArrayLong(10); /** * The result status. */ private int mResult = RESULT_UNDEFINED; /** * Instance of a ContactChange comparator. */ private NCCComparator mNCCC = new NCCComparator(); /** * The array of accounts from where to import the native contacts. Note: if * null, will import from the platform default account. */ private Account[] mAccounts = null; /** * Boolean that tracks if the first time Import for 2.X is ongoing. */ private boolean mIsFirstImportOn2X = false; /** * Constructor. * * @param pca handler to the People contacts Api * @param nca handler to the Native contacts Api * @param firstTimeImport true if we import from native for the first time, * false if not */ public NativeImporter(PeopleContactsApi pca, NativeContactsApi nca, boolean firstTimeImport) { mPeopleContactsApi = pca; mNativeContactsApi = nca; initAccounts(firstTimeImport); } /** * Sets the accounts used to import the native contacs. */ private void initAccounts(boolean firstTimeImport) { /** * In case of Android 2.X, the accounts used are different depending if * it's first time sync or not. On Android 1.X, we can just ignore the * accounts logic as not supported by the platform. At first time sync, * we need to import the native contacts from all the Google accounts. * These native contacts are then stored in the 360 People account and * native changes will be only detected from the 360 People account. */ if (VersionUtils.is2XPlatform()) { // account to import from: 360 account if created, all the google // accounts otherwise if (firstTimeImport) { ArrayList<Account> accountList = new ArrayList<Account>(); /** * 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. */ /* Account googleAccounts[] = mNativeContactsApi.getAccountsByType(NativeContactsApi.GOOGLE_ACCOUNT_TYPE); if (googleAccounts!=null ){ for (Account account : googleAccounts) { accountList.add(account); } } */ Account phoneAccounts[] = mNativeContactsApi.getAccountsByType(NativeContactsApi.PHONE_ACCOUNT_TYPE); if (phoneAccounts!=null){ for (Account account : phoneAccounts) { accountList.add(account); } } mAccounts = accountList.toArray(new Account[0]); } else { mAccounts = mNativeContactsApi .getAccountsByType(NativeContactsApi.PEOPLE_ACCOUNT_TYPE); } if (firstTimeImport) mIsFirstImportOn2X = true; } } /** * Sets the internal state to DONE with the provided result status. * * @param result the result status to set */ private void complete(int result) { mState = STATE_DONE; mResult = result; } /** * Tick method to call each time there is time for processing the native * contacts import. Note: the method will block for some time to process a * certain amount of contact then will return. It will have to be called * until it returns true meaning that the import is over. * * @return true when the import task is finished, false if not */ public boolean tick() { long startTime = System.currentTimeMillis(); switch (mState) { case STATE_GET_IDS_LISTS: getIdsLists(); break; case STATE_ITERATE_THROUGH_IDS: iterateThroughNativeIds(); break; case STATE_PROCESS_DELETED: processDeleted(); break; } long processingTime = System.currentTimeMillis() - startTime; float factor = TARGET_TIME_PER_TICK / processingTime; mContactsPerTick = Math.max(1.0F, mContactsPerTick * factor); LogUtils.logD("NativeImporter.tick(): Tick took " + processingTime + "ms, applying factor "+ factor + ". Contacts per tick: " + mContactsPerTick); return isDone(); } /** * Returns the import state. * * @return true if the import is finished, false if not */ public boolean isDone() { return mState == STATE_DONE; } /** * Gets the import result. * * @see #RESULT_OK * @see #RESULT_ERROR * @see #RESULT_UNDEFINED * @return the import result */ public int getResult() { return mResult; } /** * Gets the current position in the list of ids. This can be used to track * the current progress. * * @see #getCount() * @return the last processed id position in the list of ids */ public int getPosition() { return mProcessedIds; } /** * Gets the total number of ids to process. * * @return the number of ids to process */ public int getCount() { return mTotalIds; } /** * Gets the list of native and people contacts ids. */ private void getIdsLists() { LogUtils.logD("NativeImporter.getIdsLists()"); // Get the list of native ids for the contacts if (mAccounts == null || 0 == mAccounts.length) { // default account LogUtils.logD("NativeImporter.getIdsLists() - using default account"); mNativeContactsIds = mNativeContactsApi.getContactIds(null); } else if (mAccounts.length == 1) { // one account LogUtils.logD("NativeImporter.getIdsLists() - one account found: " + mAccounts[0]); mNativeContactsIds = mNativeContactsApi.getContactIds(mAccounts[0]); } else { // we need to merge the ids from different accounts and sort them final DynamicArrayLong allIds = new DynamicArrayLong(); LogUtils.logD("NativeImporter.getIdsLists() - more than one account found."); for (int i = 0; i < mAccounts.length; i++) { LogUtils.logD("NativeImporter.getIdsLists() - account=" + mAccounts[i]); final long[] ids = mNativeContactsApi.getContactIds(mAccounts[i]); if (ids != null) { allIds.add(ids); } } mNativeContactsIds = allIds.toArray(); // sort the ids // TODO: as the arrays to merge are sorted, consider merging while // keeping the sorting // which is faster than sorting them afterwards if (mNativeContactsIds != null) { Arrays.sort(mNativeContactsIds); } } // check if we have some work to do if (mNativeContactsIds == null) { complete(RESULT_OK); return; } // Get a list of native ids for the contacts we have in the People // database mPeopleNativeContactsIds = mPeopleContactsApi.getNativeContactsIds(); mTotalIds = mNativeContactsIds.length; if (mPeopleNativeContactsIds != null) { mTotalIds += mPeopleNativeContactsIds.length; } mState = STATE_ITERATE_THROUGH_IDS; } /** * Iterates through the list of native and People ids to detect changes. */ private void iterateThroughNativeIds() { LogUtils.logD("NativeImporter.iterateThroughNativeIds()"); final int limit = Math.min(mNativeContactsIds.length, mCurrentNativeIndex + Math.round(mContactsPerTick)); // TODO: remove the deleted state / queuing to deleted ids array and // loop with while (mProcessedIds < limit) while (mCurrentNativeIndex < limit) { if (mPeopleNativeContactsIds == null) { // no native contacts on people side, just add it LogUtils.logD("NativeImporter.iterateThroughNativeIds(): found a new contact"); addNewContact(mNativeContactsIds[mCurrentNativeIndex]); mProcessedIds++; } else { // both ids lists are ordered by ascending ids so // every people ids that are before the current native ids // are simply deleted contacts while ((mCurrentPeopleIndex < mPeopleNativeContactsIds.length) && (mPeopleNativeContactsIds[mCurrentPeopleIndex] < mNativeContactsIds[mCurrentNativeIndex])) { LogUtils .logD("NativeImporter.iterateThroughNativeIds(): found a contact to delete"); mDeletedIds.add(mPeopleNativeContactsIds[mCurrentPeopleIndex++]); } if (mCurrentPeopleIndex == mPeopleNativeContactsIds.length || mPeopleNativeContactsIds[mCurrentPeopleIndex] > mNativeContactsIds[mCurrentNativeIndex]) { // has to be a new contact LogUtils.logD("NativeImporter.iterateThroughNativeIds(): found a new contact"); addNewContact(mNativeContactsIds[mCurrentNativeIndex]); mProcessedIds++; } else { // has to be an existing contact or one that will be deleted LogUtils .logD("NativeImporter.iterateThroughNativeIds(): check existing contact"); checkExistingContact(mNativeContactsIds[mCurrentNativeIndex]); mProcessedIds++; mCurrentPeopleIndex++; } } mCurrentNativeIndex++; } // check if we are done with ids list from native if (mCurrentNativeIndex == mNativeContactsIds.length) { // we've gone through the native list, any remaining ids from the // people list are deleted ones if (mPeopleNativeContactsIds != null) { while (mCurrentPeopleIndex < mPeopleNativeContactsIds.length) { LogUtils .logD("NativeImporter.iterateThroughNativeIds(): found a contact to delete"); mDeletedIds.add(mPeopleNativeContactsIds[mCurrentPeopleIndex++]); } } if (mDeletedIds.size() != 0) { // Some deleted contacts to handle mState = STATE_PROCESS_DELETED; } else { // Nothing else to do complete(RESULT_OK); } } } /** * Deletes the contacts that were added the deleted array. */ private void processDeleted() { LogUtils.logD("NativeImporter.processDeleted()"); final int limit = Math.min(mDeletedIds.size(), mCurrentDeletedIndex + Math.round(mContactsPerTick)); while (mCurrentDeletedIndex < limit) { // we now delete the contacts on people client side // on the 2.X platform, the contact deletion has to be synced back // to native once completed because the // contact is still there on native, just marked as deleted and // waiting for its explicit removal mPeopleContactsApi.deleteNativeContact(mDeletedIds.get(mCurrentDeletedIndex++), VersionUtils.is2XPlatform()); mProcessedIds++; } if (mCurrentDeletedIndex == mDeletedIds.size()) { complete(RESULT_OK); } } /** * Adds a new contact to the people database. */ private void addNewContact(long nativeId) { // get the contact data final ContactChange[] contactChanges = mNativeContactsApi.getContact(nativeId); if (contactChanges != null) { if (mIsFirstImportOn2X) { // Override the nativeContactId with an invalid id if we are on // 2.X // and we are doing a first time import because the native id // does not correspond // to the id from the 360 People account where we will export. removeNativeIds(contactChanges); } else { // Force a nativeDetailId to details that have none so that the // comparison // later can be made (see computeDelta method). forceNabDetailId(contactChanges); } // add the contact to the People database if (!mPeopleContactsApi.addNativeContact(contactChanges)) { // TODO: Handle the error case !!! Well how should we handle it: // fail for all remaining contacts or skip the failing contact? LogUtils .logE("NativeImporter.addNewContact() - failed to import native contact id=" + nativeId); } } } /** * Check changes between an existing contact on both native and people * database. * * @param nativeId the native id of the contact to check */ private void checkExistingContact(long nativeId) { // get the native version of that contact final ContactChange[] nativeContact = mNativeContactsApi.getContact(nativeId); // get the people version of that contact final ContactChange[] peopleContact = mPeopleContactsApi.getContact((int)nativeId); if (peopleContact == null) { // we shouldn't be in that situation but nothing to do about it // this means that there were some changes in the meantime between // getting the ids list and now return; } else if (nativeContact == null) { LogUtils .logD("NativeImporter.checkExistingContact(): found a contact marked as deleted"); // this is a 2.X specific case meaning that the contact is marked as // deleted on native side // and waiting for the "syncAdapter" to perform a real delete mDeletedIds.add(nativeId); } else { // general case, find the delta final ContactChange[] delta = computeDelta(peopleContact, nativeContact); if (delta != null) { // update CAB with delta changes mPeopleContactsApi.updateNativeContact(delta); } } } /** * Native ContactChange Comparator class. This class compares ContactChange * and tells which one is greater depending on the key and then the native * detail id. */ private static class NCCComparator implements Comparator<ContactChange> { @Override public int compare(ContactChange change1, ContactChange change2) { // an integer < 0 if object1 is less than object2, 0 if they are // equal, and > 0 if object1 is greater than object2. final int key1 = change1.getKey(); final int key2 = change2.getKey(); if (key1 < key2) { return -1; } else if (key1 > key2) { return 1; } else { // the keys are identical, check the native ids final long id1 = change1.getNabDetailId(); final long id2 = change2.getNabDetailId(); if (id1 < id2) { return -1; } else if (id1 > id2) { return 1; } else { return 0; } } } } /** * Sorts the provided array of ContactChange by key and native id. Note: the * method will rearrange the provided array. * * @param changes the ContactChange array to sort */ private void sortContactChanges(ContactChange[] changes) { if ((changes != null) && (changes.length > 1)) { Arrays.sort(changes, mNCCC); } } /** * Computes the difference between the provided arrays of ContactChange. The * delta are the changes to apply to the master ContactChange array to make * it similar to the new changes ContactChange array. (i.e. delete details, * add details or modify details) NOTE: to help the GC, some provided * ContactChange may be modified and returned by the method instead of * creating new ones. * * @param masterChanges the master array of ContactChange (i.e. the original * one) * @param newChanges the new ContactChange array (i.e. contains the new * version of a contact) * @return null if there are no changes or the contact is being deleted, an * array for ContactChange to apply to the master contact if * differences where found */ private ContactChange[] computeDelta(ContactChange[] masterChanges, ContactChange[] newChanges) { LogUtils.logD("NativeImporter.computeDelta()"); // set the native contact id to details that don't have a native detail // id for the comparison as // this is also done on people database side forceNabDetailId(newChanges); // sort the changes by key then native detail id sortContactChanges(masterChanges); sortContactChanges(newChanges); // if the master contact is being deleted, ignore new changes if ((masterChanges[0].getType() & ContactChange.TYPE_DELETE_CONTACT) == ContactChange.TYPE_DELETE_CONTACT) { return null; } // details comparison, skip deleted master details final ContactChange[] deltaChanges = new ContactChange[masterChanges.length + newChanges.length]; int deltaIndex = 0; int masterIndex = 0; int newIndex = 0; while (newIndex < newChanges.length) { while ((masterIndex < masterChanges.length) && (mNCCC.compare(masterChanges[masterIndex], newChanges[newIndex]) < 0)) { final ContactChange masterChange = masterChanges[masterIndex]; if ((masterChange.getType() & ContactChange.TYPE_DELETE_DETAIL) != ContactChange.TYPE_DELETE_DETAIL && (masterChange.getNabDetailId() != ContactChange.INVALID_ID) && (isContactChangeKeySupported(masterChange))) { // this detail does not exist anymore, is not being deleted // and was synced to native // check if it can be deleted (or has to be updated) setDetailForDeleteOrUpdate(masterChanges, masterIndex); deltaChanges[deltaIndex++] = masterChanges[masterIndex]; } masterIndex++; } if ((masterIndex < masterChanges.length) && (mNCCC.compare(newChanges[newIndex], masterChanges[masterIndex]) == 0)) { // similar key and id, check for differences at value level and // flags final ContactChange masterDetail = masterChanges[masterIndex]; final ContactChange newDetail = newChanges[newIndex]; boolean different = false; if (masterDetail.getFlags() != newDetail.getFlags()) { different = true; } if (!areContactChangeValuesEqualsPlusFix(masterChanges, masterIndex, newChanges, newIndex)) { different = true; } if (different) { // found a detail to update LogUtils.logD("NativeImporter.computeDelta() - found a detail to update"); newDetail.setType(ContactChange.TYPE_UPDATE_DETAIL); newDetail.setInternalContactId(masterDetail.getInternalContactId()); newDetail.setInternalDetailId(masterDetail.getInternalDetailId()); deltaChanges[deltaIndex++] = newDetail; } masterIndex++; } else { LogUtils.logD("NativeImporter.computeDelta() - found a detail to add"); // this is a new detail newChanges[newIndex].setType(ContactChange.TYPE_ADD_DETAIL); newChanges[newIndex].setInternalContactId(masterChanges[0].getInternalContactId()); deltaChanges[deltaIndex++] = newChanges[newIndex]; } newIndex++; } while (masterIndex < masterChanges.length) { final ContactChange masterChange = masterChanges[masterIndex]; if ((masterChange.getType() & ContactChange.TYPE_DELETE_DETAIL) != ContactChange.TYPE_DELETE_DETAIL && (masterChange.getNabDetailId() != ContactChange.INVALID_ID) && (isContactChangeKeySupported(masterChange))) { // this detail does not exist anymore, is not being deleted and // was synced to native // check if it can be deleted (or has to be updated) setDetailForDeleteOrUpdate(masterChanges, masterIndex); deltaChanges[deltaIndex++] = masterChanges[masterIndex]; } masterIndex++; } if (deltaIndex == 0) { // the contact has not changed return null; } else if (deltaChanges.length == deltaIndex) { // give the detail changes return deltaChanges; } else { // give the detail changes but need to trim final ContactChange[] trim = new ContactChange[deltaIndex]; System.arraycopy(deltaChanges, 0, trim, 0, deltaIndex); return trim; } } /** * Removes the native ids in case of a first time import on the Android 2.X * platform. * * @param contact the contact to clear from native ids */ private void removeNativeIds(ContactChange[] contact) { if (contact != null) { final int count = contact.length; for (int i = 0; i < count; i++) { final ContactChange change = contact[i]; change.setNabContactId(ContactChange.INVALID_ID); change.setNabDetailId(ContactChange.INVALID_ID); } } } /** * Forces a native detail id onto a detail that does not have one by setting * it to be the native contact id. * * @param contact the contact to fix */ private void forceNabDetailId(ContactChange[] contact) { if (contact != null && contact.length > 0) { final long nativeContactId = contact[0].getNabContactId(); final int count = contact.length; for (int i = 0; i < count; i++) { final ContactChange change = contact[i]; if (change.getNabDetailId() == ContactChange.INVALID_ID) { change.setNabDetailId(nativeContactId); } } } } /* * Below this point, all the defined methods where added to support the * specific case of KEY_VCARD_ORG on Android 1.X platform. KEY_VCARD_ORG is * defined as followed: "company;department1;department2;...;departmentX" It * is a special case because this VCard key on its own is not fully * supported on the Android 1.X platform: only "company" information is. */ /** * Tells whether or not a master change is equal to a new change and * performs some modifications on the provided new change in the specific * case of KEY_VCARD_ORG on Android 1.X. * * @param masterChanges the array of master changes * @param masterIndex the index of the change within the array of master * changes * @param newChanges the array of new changes * @param newIndex the index of the change within the array of new changes * @return true if the changes are equals, false otherwise */ private boolean areContactChangeValuesEqualsPlusFix(ContactChange[] masterChanges, int masterIndex, ContactChange[] newChanges, int newIndex) { final ContactChange masterChange = masterChanges[masterIndex]; final ContactChange newChange = newChanges[newIndex]; if (VersionUtils.is2XPlatform() || masterChange.getKey() != ContactChange.KEY_VCARD_ORG) { // general case return TextUtils.equals(masterChange.getValue(), newChange.getValue()); } else { // this is a special case of Android 1.X where we have to parse the // value // in case of Organization key final String peopleCompValue = VCardHelper.parseCompanyFromOrganization(masterChange .getValue()); final String nativeCompValue = VCardHelper.parseCompanyFromOrganization(newChange .getValue()); // on 1.X, we only need to compare the company name as // department is not supported by the platform final boolean areEquals = TextUtils.equals(peopleCompValue, nativeCompValue); if (!areEquals) { // there is a difference so master change will be updated // we need to preserve the master department values if any final String masterDepartments = VCardHelper .splitDepartmentsFromOrganization(masterChange.getValue()); final ContactChange fixedNewChange = newChange.copyWithNewValue(nativeCompValue + masterDepartments); newChanges[newIndex] = fixedNewChange; } return areEquals; } } /** * Sets a detail as to be deleted or updated. Note: in the general case, the * detail has to be deleted but in the specific situation of KEY_VCARD_ORG * and Android 1.X, it may have to be updated instead * * @param contactChanges the array of ContactChange * @param index the index of the ContactChange to set in the array of * ContactChange */ private void setDetailForDeleteOrUpdate(ContactChange[] contactChanges, int index) { final ContactChange contactChange = contactChanges[index]; if (VersionUtils.is2XPlatform() || contactChange.getKey() != ContactChange.KEY_VCARD_ORG) { // general case, just set the details as deleted contactChange.setType(ContactChange.TYPE_DELETE_DETAIL); LogUtils .logD("NativeImporter.checkDetailForDeleteOrUpdate() - found a detail to delete"); } else { // this is a special case of Android 1.X // we have to set this change to an update if the change // contains departments final String masterDepartments = VCardHelper .splitDepartmentsFromOrganization(contactChange.getValue()); if (!VCardHelper.isEmptyVCardValue(masterDepartments)) { // delete the company name and keep the departments contactChange.setType(ContactChange.TYPE_UPDATE_DETAIL); contactChanges[index] = contactChange.copyWithNewValue(masterDepartments); LogUtils .logD("NativeImporter.checkDetailForDeleteOrUpdate() - found a detail to update"); } else { contactChange.setType(ContactChange.TYPE_DELETE_DETAIL); LogUtils .logD("NativeImporter.checkDetailForDeleteOrUpdate() - found a detail to delete"); } } } /** * Determines whether or not a ContactChange key is supported on native * side. Note: it also handle the non generic case of KEY_VCARD_ORG on * Android 1.X * * @param change the ContactChange to check * @return true if supported, false if not */ private boolean isContactChangeKeySupported(ContactChange change) { final boolean isSupported = mNativeContactsApi.isKeySupported(change.getKey()); if (VersionUtils.is2XPlatform() || change.getKey() != ContactChange.KEY_VCARD_ORG) { return isSupported; } else if (isSupported) { // KEY_VCARD_ORG has the following value: // "company;department1;department2..." // in case of KEY_VCARD_ORG on Android 1.X, we have to check the // support of the key // at the company level of the VCard value because the departments // are not supported // so if there is only the department, we have to return false // instead of true. final String changeCompValue = VCardHelper.parseCompanyFromOrganization(change .getValue()); if (!VCardHelper.isEmptyVCardValue(changeCompValue)) { return true; } } return false; } }