content: /*
   * 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.HashSet;
  import java.util.Map;
  
  import com.vodafone360.people.Settings;
  import com.vodafone360.people.database.DatabaseHelper;
  import com.vodafone360.people.database.tables.ContactsTable;
  import com.vodafone360.people.database.tables.StateTable;
  import com.vodafone360.people.datatypes.BaseDataType;
  import com.vodafone360.people.datatypes.Contact;
  import com.vodafone360.people.datatypes.ContactChanges;
  import com.vodafone360.people.datatypes.ContactDetail;
  import com.vodafone360.people.datatypes.VCardHelper;
  import com.vodafone360.people.engine.BaseEngine;
  import com.vodafone360.people.engine.contactsync.SyncStatus.Task;
  import com.vodafone360.people.engine.contactsync.SyncStatus.TaskStatus;
  import com.vodafone360.people.service.ServiceStatus;
  import com.vodafone360.people.service.agent.NetworkAgent;
  import com.vodafone360.people.service.io.QueueManager;
  import com.vodafone360.people.service.io.ResponseQueue.DecodedResponse;
  import com.vodafone360.people.service.io.api.Contacts;
  import com.vodafone360.people.utils.LogUtils;
  
  /**
   * Handles download of contacts from People server.
   */
  public class DownloadServerContacts extends BaseSyncProcessor {
      
      /**
       * Helper constant for converting between milliseconds and nanoseconds.
       */
      private static final long NANOSECONDS_IN_MS = 1000000L;
  
      /**
       * Enumeration which holds the various states for the download server
       * processor.
       */
      public enum InternalState {
          INITIALISING,
          FETCHING_SERVER_ID_LIST,
          FETCHING_FIRST_PAGE,
          FETCHING_NEXT_BATCH
      }
  
      /**
       * The maximum number of contacts that should be sent by the server for each
       * page request.
       */
      protected static final int MAX_DOWN_PAGE_SIZE = 25;
  
      /**
       * The number of pages that should be requested in a single RPG request
       * (using batching).
       */
      private static final int MAX_PAGES_PER_BATCH = 1;
  
      /**
       * Maximum number of database updates for each engine run (to ensure the
       * processor doesn't block the worker thread for too long).
       */
      private static final int MAX_CONTACT_CHANGES_PER_PAGE = 5;
  
      /**
       * Timeout between each run of the engine. Normally set to 0 to ensure the
       * engine will run as soon as possible. This is used to allow other engines
       * to be run between lengthy sync operations.
       */
      private static final long TIMEOUT_BETWEEN_PAGES_MS = 0;
  
      /**
       * Set to true for extra logcat trace (debug only)
       */
      private static final boolean EXTRA_DEBUG_TRACE = true;
      
      /**
       * This will be set to the current revision of the contacts in the NowPlus
       * database. For first time sync will be set to 0. The current revision
       * number is persisted in the database.
       */
      private Integer mFromRevision = null;
  
      /**
       * The revision that we require from the server. For the first page this is
       * set to -1 to indicate we want the latest revision. For subsequent pages
       * will be set to the version anchor received from the server (to ensure all
       * pages fetched are for the same version).
       */
      private Integer mToRevision = null;
  
      /**
       * Total number of pages we are expecting from the server. This is received
       * as part of the server response for the first page.
       */
      private Integer mTotalNoOfPages = null;
  
      /**
       * First page number requested in the batch. The remaining page numbers will
       * follow from this sequentially.
       */
      private int mBatchFirstPageNo;
  
      /**
       * Total number of pages requested by batch.
       */
      private int mBatchNoOfPages;
  
      /**
       * Number of pages received so far from the batch
       */
      private int mBatchPageReceivedCount;
  
      /**
       * Total number of pages done (used with {@link #mTotalNoOfPages} for
       * calculating progress)
       */
      private int mNoOfPagesDone = 0;
  
      /***
       * Number of contacts contained in the last page. This is only known once
       * the last page have been received and is a major flaw in the protocol as
       * it makes it difficult to calculate sync progress information.
       */
      private int mLastPageSize = -1;
      
      /**
       * Set of contact server IDs from the NowPlus database.
       * Used to quickly determine if a contact received from the server is a new
       * or modified contact.
       */
      private final HashSet<Long> mServerIdSet = new HashSet<Long>();
  
      /**
       * List of all contacts received from server which are also present in the
       * local database. This is a list of all contacts received from the server
       * excluding new contacts.
       */
      private final ArrayList<Contact> mContactsChangedList = new ArrayList<Contact>();
  
      /**
       * List of all contacts received from the server that need to be added to
       * the database.
       */
      private final ArrayList<Contact> mAddContactList = new ArrayList<Contact>();
  
      /**
       * List of all contacts received from the server that need to be modified in
       * the database.
       */
      private final ArrayList<Contact> mModifyContactList = new ArrayList<Contact>();
  
      /**
       * List of all contacts received from the server that need to be deleted
       * from the database.
       */
      private final ArrayList<ContactsTable.ContactIdInfo> mDeleteContactList = new ArrayList<ContactsTable.ContactIdInfo>();
  
      /**
       * List of all contact details received from the server that need to be
       * added to the local database.
       */
      private final ArrayList<ContactDetail> mAddDetailList = new ArrayList<ContactDetail>();
  
      /**
       * List of all contact details received from the server that need to be
       * modified in the local database.
       */
      private final ArrayList<ContactDetail> mModifyDetailList = new ArrayList<ContactDetail>();
  
      /**
       * List of all contact details received from the server that need to be
       * deleted from the local database.
       */
      private final ArrayList<ContactDetail> mDeleteDetailList = new ArrayList<ContactDetail>();
  
      /**
       * Maintains all the request IDs when sending a batch of requests.
       */
      protected final Map<Integer, Integer> mPageReqIds = new HashMap<Integer, Integer>();
  
      /**
       * Flag indicating that there is some data in either the
       * {@link #mAddContactList}, {@link #mModifyContactList},
       * {@link #mDeleteContactList}, {@link #mAddDetailList},
       * {@link #mModifyDetailList} or {@link #mDeleteDetailList} list.
       */
      private boolean mSyncDataPending = false;
  
      /**
       * Current internal state of the processor.
       */
      protected InternalState mInternalState = InternalState.INITIALISING;
  
      /**
       * Used to monitor how much time is spent reading or writing to the NowPlus
       * database. This is for displaying logcat information which can be used
       * when profiling the contact sync engine.
       */
      private long mDbSyncTime = 0;
  
      /**
       * Set to true when there are no more pages to fetch from the server.
       */
      private boolean mIsComplete;
  
      /**
       * Counts the number of contacts added (for use when profiling)
       */
      private long mTotalContactsAdded;
  
      /**
       * Processor constructor.
       * 
       * @param callback Callback interface for accessing the contact sync engine.
       * @param db Database used for modifying contacts
       */
      protected DownloadServerContacts(IContactSyncCallback callback, DatabaseHelper db) {
          super(callback, db);
      }
  
      /**
       * For debug only - to show when the garbage collector destroys the
       * processor
       */
      @Override
      protected void finalize() {
          LogUtils.logD("DownloadServerContacts.finalize() - processor deleted");
      }
  
      /**
       * Called by framework to start the processor running. Requests the first
       * page of contact changes from the server.
       */
      @Override
      protected void doStart() {
          setSyncStatus(new SyncStatus(0, "", Task.DOWNLOAD_SERVER_CONTACTS));
          
          mIsComplete = false;
          mSyncDataPending = false;
          mFromRevision = StateTable.fetchContactRevision(mDb.getReadableDatabase());
          if (mFromRevision == null) {
              mFromRevision = 0; // Use base
          }
          mToRevision = -1; // Sync with head revision
          mTotalNoOfPages = null;
          mBatchNoOfPages = 1;
          mBatchFirstPageNo = 0;
          mBatchPageReceivedCount = 0;
          mDbSyncTime = 0;
          mNoOfPagesDone = 0;
          mTotalContactsAdded = 0;
          mInternalState = InternalState.FETCHING_SERVER_ID_LIST;
          long startTime = System.nanoTime();
          ServiceStatus status = ContactsTable.fetchContactServerIdList(mServerIdSet, mDb.getReadableDatabase());
          if (ServiceStatus.SUCCESS != status) {
              complete(status);
          }
          mDbSyncTime += (System.nanoTime() - startTime);
          setTimeout(TIMEOUT_BETWEEN_PAGES_MS);
      }
  
      /**
       * Requests first page of contact changes from the server.
       * 
       * @return SUCCESS or a suitable error code.
       */
      private ServiceStatus fetchFirstBatch() {
          if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
              return NetworkAgent.getServiceStatusfromDisconnectReason();
          }
          mInternalState = InternalState.FETCHING_FIRST_PAGE;
          LogUtils.logD("DownloadServerContacts.fetchFirstBatch - from rev " + mFromRevision
                  + ", to rev " + mToRevision + ", page size " + MAX_DOWN_PAGE_SIZE);
          int reqId = Contacts.getContactsChanges(getEngine(), 0, MAX_DOWN_PAGE_SIZE, mFromRevision
                  .longValue(), mToRevision.longValue(), false);
          setReqId(reqId);
          return ServiceStatus.SUCCESS;
      }
  
      /**
       * Requests the next batch of contact change pages from the server
       * 
       * @return SUCCESS or a suitable error code
       */
      private ServiceStatus fetchNextBatch() {
          mBatchNoOfPages = Math.min(mTotalNoOfPages - mBatchFirstPageNo, MAX_PAGES_PER_BATCH);
          if (mBatchNoOfPages == 0) {
              return ServiceStatus.SUCCESS;
          }
          mBatchPageReceivedCount = 0;
          if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
              return NetworkAgent.getServiceStatusfromDisconnectReason();
          }
  
          mPageReqIds.clear();
          LogUtils.logD("DownloadServerContacts.fetchNextBatch - from rev " + mFromRevision
                  + ", to rev " + mToRevision + ", page size " + MAX_DOWN_PAGE_SIZE);
          for (int i = 0; i < mBatchNoOfPages; i++) {
              int reqId = Contacts.getContactsChanges(getEngine(), i + mBatchFirstPageNo,
                      MAX_DOWN_PAGE_SIZE, mFromRevision.longValue(), mToRevision.longValue(), true);
              mPageReqIds.put(reqId, i + mBatchFirstPageNo);
          }
          // AA: see if we can do that inside the Queue
          QueueManager.getInstance().fireQueueStateChanged();
          return ServiceStatus.SUCCESS;
      }
  
      /**
       * Called by framework when contact sync is cancelled. Not used, the server
       * response will simply be ignored.
       */
      @Override
      protected void doCancel() {
          LogUtils.logD("DownloadServerContacts.doCancel()");
      }
  
      /**
       * Called by framework when a response is received from the server. In case
       * of the first page this will only be called if the request ID matches.
       * Processes the page and if the page is the last one in the batch, sends a
       * new batch of requests to the server.
       * 
       * @param response from server
       */
      @Override
      public void processCommsResponse(DecodedResponse resp) {
          Integer pageNo = 0;
          if (mInternalState == InternalState.FETCHING_NEXT_BATCH) {
              pageNo = mPageReqIds.remove(resp.mReqId);
              if (pageNo == null) {
                  LogUtils.logD("DownloadServerContacts.processCommsResponse: Req ID not known");
                  return;
              }
          }
          LogUtils.logD("DownloadServerContacts.processCommsResponse() - Page " + pageNo);
          ServiceStatus status = BaseEngine.getResponseStatus(BaseDataType.CONTACT_CHANGES_DATA_TYPE,
                  resp.mDataTypes);
          if (status == ServiceStatus.SUCCESS) {
              ContactChanges contactChanges = (ContactChanges)resp.mDataTypes.get(0);
              LogUtils.logI("DownloadServerContacts.processCommsResponse - No of contacts = "
                      + contactChanges.mContacts.size());
              if (contactChanges.mContacts.size() == 0 && pageNo > 0) {
                  LogUtils.logW("DownloadServerContacts.processCommsResponse - "
                          + "Error a page with 0 contacts was received");
                  LogUtils.logW("DownloadServerContacts.processCommsResponse - Changes = "
                          + contactChanges);
              }
              mBatchPageReceivedCount++;
              if (mBatchPageReceivedCount == mBatchNoOfPages) {
                  mLastPageSize = contactChanges.mContacts.size();
                  
                  // Page batch is now complete
                  if (mInternalState == InternalState.FETCHING_FIRST_PAGE) {
                      mTotalNoOfPages = contactChanges.mNumberOfPages;
                      mToRevision = contactChanges.mVersionAnchor;
                      mInternalState = InternalState.FETCHING_NEXT_BATCH;
                  }
                  mBatchFirstPageNo += mBatchNoOfPages;
                  if (mTotalNoOfPages == null || mToRevision == null) {
                      complete(ServiceStatus.ERROR_COMMS_BAD_RESPONSE);
                      return;
                  }
                  if (mBatchFirstPageNo < mTotalNoOfPages.intValue()) {
                      status = fetchNextBatch();
                      if (ServiceStatus.SUCCESS != status) {
                          complete(status);
                          return;
                      }
                  } else {
                      mIsComplete = true;
                  }
              }
              
    
              
              status = syncContactChangesPage(contactChanges);
              mNoOfPagesDone++;
              LogUtils.logI("DownloadServerContacts.processCommsResponse() - Contact changes page "
                      + mNoOfPagesDone + "/" + mTotalNoOfPages + " received, no of contacts = "
                      + contactChanges.mContacts.size());
              if (ServiceStatus.SUCCESS != status) {
                  LogUtils.logE("DownloadServerContacts.processCommsResponse() - Error syncing page: " + status);
                  complete(status);
                  return;
              }
              if (mIsComplete && mContactsChangedList.size() == 0 && !mSyncDataPending) {
                  downloadSyncSuccessful();
              }
              return;
          }
          complete(status);
      }
      
      /***
       * Check the given contact to see if it has a valid name, and if so send it
       * to be shown in the contacts sync progress UI.
       * 
       * @param contact Contact which has been downloaded.
       * @param incOfCurrentPage Number of contacts that have so far been
       *            downloaded for the current page.
       */
      private void updateProgressUi(Contact contact, int incOfCurrentPage) {
          String name = "";
          
          if (contact != null) {
              ContactDetail contactDetail = contact.getContactDetail(ContactDetail.DetailKeys.VCARD_NAME);
              if (contactDetail != null) {
                  VCardHelper.Name vCardHelperName = contactDetail.getName();
                  if (vCardHelperName != null) {
                      name = vCardHelperName.toString();
                  }
              }
          }
  
          int totalNumberOfContacts = ((mTotalNoOfPages*10)-1)*MAX_DOWN_PAGE_SIZE/10;    
          if (mLastPageSize != -1) {
              totalNumberOfContacts = (mTotalNoOfPages-1) * MAX_DOWN_PAGE_SIZE + mLastPageSize;
          }
  
          int progress = (incOfCurrentPage + (mNoOfPagesDone * MAX_DOWN_PAGE_SIZE))*100 / totalNumberOfContacts;
          setSyncStatus(new SyncStatus(progress, name,
                  Task.DOWNLOAD_SERVER_CONTACTS,
                  TaskStatus.RECEIVED_CONTACTS,
                  incOfCurrentPage + mNoOfPagesDone * MAX_DOWN_PAGE_SIZE,
                  totalNumberOfContacts));
      }
  
      /**
       * Processes the response from the server. First separates new contacts from
       * existing ones, this is to optimise adding new contacts during first time
       * sync.
       * 
       * @param changes The contact change information received from the server
       * @return SUCCESS or a suitable error code
       */
      private ServiceStatus syncContactChangesPage(ContactChanges changes) {
          mContactsChangedList.clear();
          
          int i = 0;
          for (Contact contact : changes.mContacts) {
              i += 1;
              updateProgressUi(contact, i);
              
              if (Settings.ENABLED_CONTACTS_SYNC_TRACE) {
                  if (contact.details.size() == 0 || contact.deleted != null) {
                      LogUtils.logD("Contact In: " + contact.contactID + ", Del: " + contact.deleted
                              + ", details: " + contact.details.size());
                  }
                  for (ContactDetail detail : contact.details) {
                      LogUtils.logD("Contact In: " + contact.contactID + ", Detail: " + detail.key + ", "
                              + detail.unique_id + ", " + detail.keyType + " = " + detail.value + ", del = "
                              + detail.deleted);
                  }
              }
              if (mServerIdSet.contains(contact.contactID)) {
                  mContactsChangedList.add(contact);
                  
              } else {
                  if (contact.deleted != null && contact.deleted.booleanValue()) {
                      if (EXTRA_DEBUG_TRACE) {
                          LogUtils.logD("DownloadServerContacts.syncDownContact() "
                                  + "Delete Contact (nothing to do) " + contact.contactID);
                      }
                  } else if (contact.details.size() > 0) {
                      if (EXTRA_DEBUG_TRACE) {
                          LogUtils.logD("DownloadServerContacts.syncDownContact() Adding "
                                  + contact.contactID);
                      }
                      mAddContactList.add(contact);
                      mSyncDataPending = true;
                  } else {
                      if (EXTRA_DEBUG_TRACE) {
                          LogUtils.logD("DownloadServerContacts.syncDownContact() Empty "
                                  + contact.contactID);
                      }
                  }
              }
          }
          if (mSyncDataPending || mContactsChangedList.size() > 0) {
              setTimeout(TIMEOUT_BETWEEN_PAGES_MS);
          }
          return ServiceStatus.SUCCESS;
      }
  
      /**
       * Processes next page of modified contacts in the
       * {@link #mContactsChangedList}.
       */
      private void processContactChangesNextPage() {
          final int count = Math.min(mContactsChangedList.size(), MAX_CONTACT_CHANGES_PER_PAGE);
          for (int i = 0; i < count; i++) {
              final Contact srcContact = mContactsChangedList.get(0);
              mContactsChangedList.remove(0);
              final Contact destContact = new Contact();
              long startTime = System.nanoTime();
              ServiceStatus status = mDb.fetchContactByServerId(srcContact.contactID, destContact);
              if (ServiceStatus.SUCCESS != status) {
                  LogUtils
                          .logE("DownloadServerContacts.processContactChangesNextPage() - Error syncing page: "
                                  + status);
                  complete(status);
                  return;
              }
              mDbSyncTime += (System.nanoTime() - startTime);
              status = syncDownContact(srcContact, destContact);
              if (ServiceStatus.SUCCESS != status) {
                  LogUtils
                          .logE("DownloadServerContacts.processContactChangesNextPage() - Error syncing page: "
                                  + status);
                  complete(status);
                  return;
              }
          }
          if (mContactsChangedList.size() == 0 && !mSyncDataPending) {
              if (mIsComplete) {
                  downloadSyncSuccessful();
              }
              return;
          }
  
          setTimeout(TIMEOUT_BETWEEN_PAGES_MS);
      }
  
      /**
       * Modifies a contact from the NowPlus database to match the one received
       * from the server
       * 
       * @param srcContact Contact received from the server
       * @param destContact Contact as stored in the database
       * @return SUCCESS or a suitable error code.
       */
      private ServiceStatus syncDownContact(Contact srcContact, Contact destContact) {
          final long localContactId = destContact.localContactID;
          final Integer nativeContactId = destContact.nativeContactId;
          if (srcContact.deleted != null && srcContact.deleted.booleanValue()) {
              if (EXTRA_DEBUG_TRACE) {
                  LogUtils.logD("DownloadServerContacts.syncDownContact - Deleting contact "
                          + srcContact.contactID);
              }
              ContactsTable.ContactIdInfo idInfo = new ContactsTable.ContactIdInfo();
              idInfo.localId = localContactId;
              idInfo.serverId = srcContact.contactID;
              idInfo.nativeId = nativeContactId;
              mDeleteContactList.add(idInfo);
              mSyncDataPending = true;
              return ServiceStatus.SUCCESS;
          }
  
          if (checkContactMods(srcContact, destContact)) {
              if (EXTRA_DEBUG_TRACE) {
                  LogUtils.logD("DownloadServerContacts.syncDownContact - Modifying contact "
                          + srcContact.contactID);
              }
              srcContact.localContactID = localContactId;
              srcContact.nativeContactId = nativeContactId;
              mModifyContactList.add(srcContact);
              mSyncDataPending = true;
          }
  
          if (srcContact.details == null) {
              if (EXTRA_DEBUG_TRACE) {
                  LogUtils.logD("DownloadServerContacts.syncDownContact - No details changed "
                          + srcContact.contactID);
              }
              return ServiceStatus.SUCCESS;
          }
  
          for (ContactDetail srcDetail : srcContact.details) {
              srcDetail.localContactID = localContactId;
              srcDetail.nativeContactId = nativeContactId;
              boolean detailFound = false;
              for (ContactDetail destDetail : destContact.details) {
                  if (DatabaseHelper.doDetailsMatch(srcDetail, destDetail)) {
                      detailFound = true;
                      srcDetail.localDetailID = destDetail.localDetailID;
                      srcDetail.nativeContactId = destDetail.nativeContactId;
                      srcDetail.nativeDetailId = destDetail.nativeDetailId;
                      srcDetail.serverContactId = srcContact.contactID;
                      if (srcDetail.deleted != null && srcDetail.deleted.booleanValue()) {
                          if (EXTRA_DEBUG_TRACE) {
                              LogUtils
                                      .logD("DownloadServerContacts.syncDownContact - Deleting detail "
                                              + srcDetail.key
                                              + " contact ID = "
                                              + srcContact.contactID);
                          }
                          mDeleteDetailList.add(srcDetail);
                          mSyncDataPending = true;
                      } else if (DatabaseHelper.hasDetailChanged(destDetail, srcDetail)) {
                          if (EXTRA_DEBUG_TRACE) {
                              LogUtils
                                      .logD("DownloadServerContacts.syncDownContact - Modifying detail "
                                              + srcDetail.key
                                              + " local detail ID = "
                                              + srcDetail.localDetailID);
                          }
                          srcDetail.serverContactId = srcContact.contactID;
                          mModifyDetailList.add(srcDetail);
                          mSyncDataPending = true;
                      }
                      break;
                  }
              }
              if (!detailFound) {
                  if (srcDetail.deleted == null || !srcDetail.deleted.booleanValue()) {
                      if (EXTRA_DEBUG_TRACE) {
                          LogUtils
                                  .logD("DownloadServerContacts.syncDownContact - Adding detail "
                                          + srcDetail.key + " local contact ID = "
                                          + srcDetail.localContactID);
                      }
                      mAddDetailList.add(srcDetail);
                      mSyncDataPending = true;
                  } else {
                      if (EXTRA_DEBUG_TRACE) {
                          LogUtils
                                  .logD("DownloadServerContacts.syncDownContact - Detail already deleted (nothing to do) "
                                          + srcDetail.key
                                          + " local detail ID = "
                                          + srcDetail.localDetailID);
                      }
                  }
              }
          }
          return ServiceStatus.SUCCESS;
      }
  
      /**
       * Compares two contacts to check for any differences. Implemented to avoid
       * updating the database when nothing has changed (to improve performance).
       * 
       * @param srcContact Contact received from the server
       * @param destContact Contact as it is in the database.
       * @return true if the contacts are different, false otherwise.
       */
      private boolean checkContactMods(Contact srcContact, Contact destContact) {
          boolean contactModified = false;
          if (srcContact.friendOfMine != null
                  && !srcContact.friendOfMine.equals(destContact.friendOfMine)) {
              destContact.friendOfMine = srcContact.friendOfMine;
              contactModified = true;
          }
          if (srcContact.gender != null && !srcContact.gender.equals(destContact.gender)) {
              destContact.gender = srcContact.gender;
              contactModified = true;
          }
          if (srcContact.sources != null) {
              boolean changed = false;
              if (srcContact.sources.size() != destContact.sources.size()) {
                  changed = true;
              } else {
                  for (int i = 0; i < srcContact.sources.size(); i++) {
                      if (srcContact.sources.get(i) != null
                              && !srcContact.sources.get(i).equals(destContact.sources.get(i))) {
                          changed = true;
                      }
                  }
              }
              if (changed) {
                  destContact.sources.clear();
                  destContact.sources.addAll(srcContact.sources);
                  contactModified = true;
              }
          }
          if (srcContact.userID != null && !srcContact.userID.equals(destContact.userID)) {
              destContact.userID = srcContact.userID;
              contactModified = true;
          }
          if (srcContact.aboutMe != null && !srcContact.aboutMe.equals(destContact.aboutMe)) {
              destContact.aboutMe = srcContact.aboutMe;
              contactModified = true;
          }
          if (srcContact.groupList != null) {
              boolean changed = false;
              if (srcContact.groupList.size() != destContact.groupList.size()) {
                  changed = true;
              } else {
                  for (int i = 0; i < srcContact.groupList.size(); i++) {
                      if (srcContact.groupList.get(i) != null
                              && !srcContact.groupList.get(i).equals(destContact.groupList.get(i))) {
                          changed = true;
                      }
                  }
              }
              if (changed) {
                  destContact.groupList.clear();
                  destContact.groupList.addAll(srcContact.groupList);
                  contactModified = true;
              }
          }
          if (srcContact.synctophone != null && !srcContact.synctophone.equals(destContact.synctophone)) {
              destContact.synctophone = srcContact.synctophone;
              contactModified = true;
          }
  
          return contactModified;
      }
  
      /**
       * Called by framework when a timeout expires. Timeouts are used by this
       * processor to break up lengthy database update tasks. A timeout of 0 is
       * normally used just to give other engines a chance to run.
       */
      public void onTimeoutEvent() {
          switch (mInternalState) {
              case FETCHING_SERVER_ID_LIST:
                  ServiceStatus status = fetchFirstBatch();
                  if (ServiceStatus.SUCCESS != status) {
                      complete(status);
                      return;
                  }
                  break;
              case FETCHING_FIRST_PAGE:
              case FETCHING_NEXT_BATCH:
                  if (mContactsChangedList.size() > 0) {
                      processContactChangesNextPage();
                      return;
                  }
                  if (mSyncDataPending) {
                      if (addContactList()) {
                          setTimeout(TIMEOUT_BETWEEN_PAGES_MS);
                          return;
                      }
                      if (modifyContactList()) {
                          setTimeout(TIMEOUT_BETWEEN_PAGES_MS);
                          return;
                      }
                      if (deleteContactList()) {
                          setTimeout(TIMEOUT_BETWEEN_PAGES_MS);
                          return;
                      }
                      if (addDetailList()) {
                          setTimeout(TIMEOUT_BETWEEN_PAGES_MS);
                          return;
                      }
                      if (modifyDetailList()) {
                          setTimeout(TIMEOUT_BETWEEN_PAGES_MS);
                          return;
                      }
                      if (deleteDetailList()) {
                          setTimeout(TIMEOUT_BETWEEN_PAGES_MS);
                          return;
                      }
  
                      mSyncDataPending = false;
                      if (mIsComplete) {
                          downloadSyncSuccessful();
                          return;
                      }
                  }
                  break;
              default:
                  // do nothing.
                  break;
          }
      }
  
      /**
       * Adds contacts received from the server (listed in the
       * {@link #mAddContactList} list) into the local database.
       * 
       * @return true if > 0 contacts were added, false otherwise.
       */
      private boolean addContactList() {
          if (mAddContactList.size() == 0) {
              return false;
          }
          LogUtils.logI("DownloadServerContacts.addContactList " + mAddContactList.size()
                  + " contacts...");
          long startTime = System.nanoTime();
          ServiceStatus status = mDb.syncAddContactList(mAddContactList, false, true);
          if (ServiceStatus.SUCCESS != status) {
              complete(status);
              return true;
          }
          markDbChanged();
          long timeDiff = System.nanoTime() - startTime;
          mDbSyncTime += timeDiff;
          LogUtils.logI("DownloadServerContacts.addContactList - time = "
                  + (timeDiff / NANOSECONDS_IN_MS) + "ms, total = "
                  + (mDbSyncTime / NANOSECONDS_IN_MS) + "ms");
          setTimeout(TIMEOUT_BETWEEN_PAGES_MS);
          mTotalContactsAdded += mAddContactList.size();
          mAddContactList.clear();
          return true;
      }
  
      /**
       * Updates the local database with contacts received from the server (listed
       * in the {@link #mModifyContactList} list).
       * 
       * @return true if > 0 contacts were modified, false otherwise.
       */
      private boolean modifyContactList() {
          if (mModifyContactList.size() == 0) {
              return false;
          }
          LogUtils.logI("DownloadServerContacts.modifyContactList " + mModifyContactList.size()
                  + " contacts...");
          long startTime = System.nanoTime();
          ServiceStatus status = mDb.syncModifyContactList(mModifyContactList, false, true);
          if (ServiceStatus.SUCCESS != status) {
              complete(status);
              return true;
          }
          markDbChanged();
          long timeDiff = System.nanoTime() - startTime;
          mDbSyncTime += timeDiff;
          LogUtils.logI("DownloadServerContacts.modifyContactList - time = "
                  + (timeDiff / NANOSECONDS_IN_MS) + "ms, total = "
                  + (mDbSyncTime / NANOSECONDS_IN_MS) + "ms");
          mModifyContactList.clear();
          return true;
      }
  
      /**
       * Deletes contacts from the local database based on changes received from
       * the server (listed in the {@link #mDeleteContactList} list).
       * 
       * @return true if > 0 contacts were deleted, false otherwise.
       */
      private boolean deleteContactList() {
          if (mDeleteContactList.size() == 0) {
              return false;
          }
          LogUtils.logI("DownloadServerContacts.deleteContactList " + mDeleteContactList.size()
                  + " contacts...");
          long startTime = System.nanoTime();
          ServiceStatus status = mDb.syncDeleteContactList(mDeleteContactList, false, true);
          if (ServiceStatus.SUCCESS != status) {
              complete(status);
              return true;
          }
          markDbChanged();
          long timeDiff = System.nanoTime() - startTime;
          mDbSyncTime += timeDiff;
          LogUtils.logI("DownloadServerContacts.deleteContactList - time = "
                  + (timeDiff / NANOSECONDS_IN_MS) + "ms, total = "
                  + (mDbSyncTime / NANOSECONDS_IN_MS) + "ms");
          mDeleteContactList.clear();
          return true;
      }
  
      /**
       * Adds contact details to the local database based on changes received from
       * the server (listed in the {@link #mAddDetailList} list).
       * 
       * @return true if > 0 contact details were added, false otherwise.
       */
      private boolean addDetailList() {
          if (mAddDetailList.size() == 0) {
              return false;
          }
  
          LogUtils.logI("DownloadServerContacts.addDetailList " + mAddDetailList.size()
                  + " contact details...");
          long startTime = System.nanoTime();
          ServiceStatus status = mDb.syncAddContactDetailList(mAddDetailList, false, true);
          if (ServiceStatus.SUCCESS != status) {
              complete(status);
              return true;
          }
          markDbChanged();
          long timeDiff = System.nanoTime() - startTime;
          mDbSyncTime += timeDiff;
          LogUtils.logI("DownloadServerContacts.addDetailList - time = "
                  + (timeDiff / NANOSECONDS_IN_MS) + "ms, total = "
                  + (mDbSyncTime / NANOSECONDS_IN_MS) + "ms");
          mAddDetailList.clear();
          return true;
      }
  
      /**
       * Modifies contact details in the local database based on changes received
       * from the server (listed in the {@link #mModifyDetailList} list).
       * 
       * @return true if > 0 contact details were modified, false otherwise.
       */
      private boolean modifyDetailList() {
          if (mModifyDetailList.size() == 0) {
              return false;
          }
  
          LogUtils.logI("DownloadServerContacts.modifyDetailList " + mModifyDetailList.size()
                  + " contact details...");
          long startTime = System.nanoTime();
          ServiceStatus status = mDb.syncModifyContactDetailList(mModifyDetailList, false, true);
          if (ServiceStatus.SUCCESS != status) {
              complete(status);
              return true;
          }
          markDbChanged();
          long timeDiff = System.nanoTime() - startTime;
          mDbSyncTime += timeDiff;
          LogUtils.logI("DownloadServerContacts.modifyDetailList - time = "
                  + (timeDiff / NANOSECONDS_IN_MS) + "ms, total = "
                  + (mDbSyncTime / NANOSECONDS_IN_MS) + "ms");
          mModifyDetailList.clear();
          return true;
      }
  
      /**
       * Deletes contact details from the local database based on changes received
       * from the server (listed in the {@link #mDeleteDetailList} list).
       * 
       * @return true if > 0 contact details were deleted, false otherwise.
       */
      private boolean deleteDetailList() {
          if (mDeleteDetailList.size() == 0) {
              return false;
          }
          LogUtils.logI("DownloadServerContacts.deleteDetailList " + mDeleteDetailList.size()
                  + " contact details...");
          long startTime = System.nanoTime();
          ServiceStatus status = mDb.syncDeleteContactDetailList(mDeleteDetailList, false, true);
          if (ServiceStatus.SUCCESS != status) {
              complete(status);
              return true;
          }
          markDbChanged();
          long timeDiff = System.nanoTime() - startTime;
          mDbSyncTime += timeDiff;
          LogUtils.logI("DownloadServerContacts.deleteDetailList - time = "
                  + (timeDiff / NANOSECONDS_IN_MS) + "ms, total = "
                  + (mDbSyncTime / NANOSECONDS_IN_MS) + "ms");
          mDeleteDetailList.clear();
          return true;
      }
  
      /**
       * Called when the processor has finished downloading the contact changes.
       * Notifies the contact sync engine.
       */
      private void downloadSyncSuccessful() {
          LogUtils.logI("DownloadServerContacts.downloadSyncSuccessful() - Total DB access time = "
                  + (mDbSyncTime / NANOSECONDS_IN_MS) + "ms, no of contacts added = "
                  + mTotalContactsAdded);
          StateTable.modifyContactRevision(mToRevision, mDb.getWritableDatabase());
          complete(ServiceStatus.SUCCESS);
      }
  }
  