/* * CDDL HEADER START * * The contents of this file are subject to the terms of the Common Development * and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at * src/com/vodafone360/people/VODAFONE.LICENSE.txt or * http://github.com/360/360-Engine-for-Android * See the License for the specific language governing permissions and * limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each file and * include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the fields * enclosed by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved. * Use is subject to license terms. */ package com.vodafone360.people.engine.contactsync; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import android.database.Cursor; import com.vodafone360.people.Settings; import com.vodafone360.people.database.DatabaseHelper; import com.vodafone360.people.database.DatabaseHelper.ServerIdInfo; import com.vodafone360.people.database.tables.ContactChangeLogTable; import com.vodafone360.people.database.tables.ContactDetailsTable; import com.vodafone360.people.database.tables.ContactsTable; import com.vodafone360.people.database.tables.ContactChangeLogTable.ContactChangeInfo; import com.vodafone360.people.database.tables.ContactChangeLogTable.ContactChangeType; import com.vodafone360.people.database.tables.ContactsTable.ContactIdInfo; 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.ContactDetailDeletion; import com.vodafone360.people.datatypes.ContactListResponse; import com.vodafone360.people.datatypes.GroupItem; import com.vodafone360.people.datatypes.ItemList; import com.vodafone360.people.datatypes.StatusMsg; 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.ResponseQueue.DecodedResponse; import com.vodafone360.people.service.io.api.Contacts; import com.vodafone360.people.service.io.api.GroupPrivacy; import com.vodafone360.people.utils.LogUtils; import com.vodafone360.people.utils.VersionUtils; /** * Processor handling upload of contacts to the People server. */ public class UploadServerContacts extends BaseSyncProcessor { /** * Internal states supported by the processor. */ protected enum InternalState { /** Internal state when processing new contacts. **/ PROCESSING_NEW_CONTACTS, /** Internal state when processing modified details. **/ PROCESSING_MODIFIED_DETAILS, /** Internal state when processing deleted contacts. **/ PROCESSING_DELETED_CONTACTS, /** Internal state when processing deleted details. **/ PROCESSING_DELETED_DETAILS, /** Internal state when processing group additions. **/ PROCESSING_GROUP_ADDITIONS, /** Internal state when processing group deletions. **/ PROCESSING_GROUP_DELETIONS } /** * Maximum progress percentage value. */ private static final int MAX_PROGESS = 100; /** * Used for converting between nanoseconds and milliseconds. */ private static final long NANOSECONDS_IN_MS = 1000000L; /** * Maximum number of contacts to send to the server in a request. */ protected static final int MAX_UP_PAGE_SIZE = 25; /** * No of items which were sent in the current batch, used for updating the * progress bar once a particular call is complete. */ private long mNoOfItemsSent; /** * Total number of items which need to be sync'ed to the server (includes * all types of updates). Used for updating the progress. */ private int mTotalNoOfItems; /** * Total number of contacts sync'ed so far. */ private int mItemsDone; /** * Contains the next page of changes from the server change log. */ private final List<ContactChangeInfo> mContactChangeInfoList = new ArrayList<ContactChangeInfo>(); /** * Contains the next page of contacts being sent to the server which have * either been added or changed. */ private final List<Contact> mContactChangeList = new ArrayList<Contact>(); /** * Current internal state of the processor. */ private InternalState mInternalState; /** * Used for debug to monitor the database read/write time during a sync. */ private long mDbSyncTime = 0; /** * Cursor used for fetching new/modified contacts from the NowPlus database. */ private Cursor mContactsCursor; /** * List of server IDs which is sent to the server by the {@code * DeleteContacts} API. */ private final List<Long> mContactIdList = new ArrayList<Long>(); /** * Current list of groups being added to the contacts listed in the * {@link #mContactIdList}. Will only contain a single group (list is needed * for the * {@link GroupPrivacy#addContactGroupRelations(BaseEngine, List, List)} * API). */ private final List<GroupItem> mGroupList = new ArrayList<GroupItem>(); /** * Contains the main contact during the contact detail deletion process. */ private Contact mDeleteDetailContact = null; /** * Contains the server ID of the group being associated with a contact. */ private Long mActiveGroupId = null; /** * Processor constructor. * * @param callback Provides access to contact sync engine callback * functions. * @param db NowPlus Database needed for reading contact and group * information */ public UploadServerContacts(final IContactSyncCallback callback, final DatabaseHelper db) { super(callback, db); } /** * Called by framework to start the processor running. First sends all the * new contacts to the server. */ @Override protected final void doStart() { setSyncStatus(new SyncStatus(0, "", Task.UPDATE_SERVER_CONTACTS)); long startTime = System.nanoTime(); mTotalNoOfItems = ContactChangeLogTable.fetchNoOfContactDetailChanges(null, mDb .getReadableDatabase()) + ContactDetailsTable.syncServerFetchNoOfChanges(mDb.getReadableDatabase()); mDbSyncTime += (System.nanoTime() - startTime); mItemsDone = 0; startProcessNewContacts(); } /** * Sends the first page of new contacts to the server. */ private void startProcessNewContacts() { mInternalState = InternalState.PROCESSING_NEW_CONTACTS; long startTime = System.nanoTime(); mContactsCursor = ContactDetailsTable.syncServerFetchContactChanges(mDb .getReadableDatabase(), true); mDbSyncTime += (System.nanoTime() - startTime); if (mContactsCursor == null) { complete(ServiceStatus.ERROR_DATABASE_CORRUPT); } else { sendNextContactAdditionsPage(); } } /** * Sends the first page of contact modifications (includes new and modified * details) to the server. */ private void startProcessModifiedDetails() { mInternalState = InternalState.PROCESSING_MODIFIED_DETAILS; /** Cleanup unused objects **/ if (mContactsCursor != null) { mContactsCursor.close(); mContactsCursor = null; } long startTime = System.nanoTime(); mContactsCursor = ContactDetailsTable.syncServerFetchContactChanges(mDb .getReadableDatabase(), false); mDbSyncTime += (System.nanoTime() - startTime); sendNextDetailChangesPage(); } /** * Sends the first page of deleted contacts to the server. */ private void startProcessDeletedContacts() { mInternalState = InternalState.PROCESSING_DELETED_CONTACTS; /** Cleanup unused objects **/ mContactChangeList.clear(); if (mContactsCursor != null) { mContactsCursor.close(); mContactsCursor = null; } sendNextDeleteContactsPage(); } /** * Sends the deleted details of the first contact to the server. */ private void startProcessDeletedDetails() { mInternalState = InternalState.PROCESSING_DELETED_DETAILS; sendNextDeleteDetailsPage(); } /** * Sends the first contact/group relation addition request to the server. */ private void startProcessGroupAdditions() { mInternalState = InternalState.PROCESSING_GROUP_ADDITIONS; sendNextAddGroupRelationsPage(); } /** * Sends the first contact/group relation deletion request to the server. */ private void startProcessGroupDeletions() { mInternalState = InternalState.PROCESSING_GROUP_DELETIONS; sendNextDelGroupRelationsPage(); } /** * Sends the next page of new contacts to the server. */ private void sendNextContactAdditionsPage() { long startTime = System.nanoTime(); ContactDetailsTable.syncServerGetNextNewContactDetails(mContactsCursor, mContactChangeList, MAX_UP_PAGE_SIZE); mDbSyncTime += (System.nanoTime() - startTime); if (mContactChangeList.size() == 0) { moveToNextState(); return; } if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) { complete(NetworkAgent.getServiceStatusfromDisconnectReason()); return; } /** Debug output. **/ if (Settings.ENABLED_CONTACTS_SYNC_TRACE) { LogUtils.logI("UploadServerContacts." + "sendNextContactAdditionsPage() New Contacts:"); for (Contact contact : mContactChangeList) { for (ContactDetail detail : contact.details) { LogUtils.logI("UploadServerContacts." + "sendNextContactAdditionsPage() Contact: " + contact.localContactID + ", Detail: " + detail.key + ", " + detail.keyType + " = " + detail.value); } } } /** End debug output. **/ mNoOfItemsSent = mContactChangeList.size(); setReqId(Contacts.bulkUpdateContacts(getEngine(), mContactChangeList)); } /** * Sends the next page of new/modified details to the server. */ private void sendNextDetailChangesPage() { mContactChangeList.clear(); long startTime = System.nanoTime(); ContactDetailsTable.syncServerGetNextNewContactDetails(mContactsCursor, mContactChangeList, MAX_UP_PAGE_SIZE); mDbSyncTime += (System.nanoTime() - startTime); if (mContactChangeList.size() == 0) { moveToNextState(); return; } /** Debug output. **/ if (Settings.ENABLED_CONTACTS_SYNC_TRACE) { LogUtils.logI("UploadServerContacts.sendNextDetailChangesPage() " + "Contact detail changes:"); for (Contact c : mContactChangeList) { for (ContactDetail d : c.details) { LogUtils.logI("UploadServerContacts." + "sendNextDetailChangesPage() Contact: " + c.contactID + ", Detail: " + d.key + ", " + d.unique_id + " = " + d.value); } } } /** End debug output. **/ if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) { complete(NetworkAgent.getServiceStatusfromDisconnectReason()); return; } mNoOfItemsSent = mContactChangeList.size(); setReqId(Contacts.bulkUpdateContacts(getEngine(), mContactChangeList)); } /** * Sends the next page of deleted contacts to the server. */ private void sendNextDeleteContactsPage() { mContactChangeInfoList.clear(); long startTime = System.nanoTime(); if (!ContactChangeLogTable.fetchContactChangeLog(mContactChangeInfoList, ContactChangeType.DELETE_CONTACT, 0, MAX_UP_PAGE_SIZE, mDb.getReadableDatabase())) { LogUtils.logE("UploadServerContacts.sendNextDeleteContactsPage() " + "Unable to fetch contact changes from database"); complete(ServiceStatus.ERROR_DATABASE_CORRUPT); return; } mDbSyncTime += (System.nanoTime() - startTime); if (mContactChangeInfoList.size() == 0) { moveToNextState(); return; } mContactIdList.clear(); for (ContactChangeInfo info : mContactChangeInfoList) { if (info.mServerContactId != null) { mContactIdList.add(info.mServerContactId); } } if (mContactIdList.size() == 0) { startTime = System.nanoTime(); mDb.deleteContactChanges(mContactChangeInfoList); mDbSyncTime += (System.nanoTime() - startTime); moveToNextState(); return; } /** Debug output. **/ if (Settings.ENABLED_CONTACTS_SYNC_TRACE) { LogUtils.logI("UploadServerContacts.sendNextDeleteContactsPage() " + "Contacts deleted:"); for (Long id : mContactIdList) { LogUtils.logI("UploadServerContacts." + "sendNextDeleteContactsPage() Contact Id: " + id); } } /** Debug output. **/ if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) { complete(NetworkAgent.getServiceStatusfromDisconnectReason()); return; } mNoOfItemsSent = mContactIdList.size(); setReqId(Contacts.deleteContacts(getEngine(), mContactIdList)); } /** * Sends the next deleted details to the server. This has to be done one * contact at a time. */ private void sendNextDeleteDetailsPage() { mContactChangeInfoList.clear(); long startTime = System.nanoTime(); List<ContactChangeInfo> groupInfoList = new ArrayList<ContactChangeInfo>(); if (!ContactChangeLogTable.fetchContactChangeLog(groupInfoList, ContactChangeType.DELETE_DETAIL, 0, MAX_UP_PAGE_SIZE, mDb.getReadableDatabase())) { LogUtils.logE("UploadServerContacts.sendNextDeleteDetailsPage() " + "Unable to fetch contact changes from database"); complete(ServiceStatus.ERROR_DATABASE_CORRUPT); return; } mDbSyncTime += (System.nanoTime() - startTime); if (groupInfoList.size() == 0) { moveToNextState(); return; } mDeleteDetailContact = new Contact(); List<ContactChangeInfo> deleteInfoList = new ArrayList<ContactChangeInfo>(); int i = 0; for (i = 0; i < groupInfoList.size(); i++) { ContactChangeInfo info = groupInfoList.get(i); if (info.mServerContactId == null) { info.mServerContactId = mDb.fetchServerId(info.mLocalContactId); } if (info.mServerContactId != null) { mDeleteDetailContact.localContactID = info.mLocalContactId; mDeleteDetailContact.contactID = info.mServerContactId; break; } deleteInfoList.add(info); info.mLocalContactId = null; } mDb.deleteContactChanges(deleteInfoList); if (mDeleteDetailContact.contactID == null) { moveToNextState(); return; } mContactChangeInfoList.clear(); for (; i < groupInfoList.size(); i++) { ContactChangeInfo info = groupInfoList.get(i); if (info.mLocalContactId != null && info.mLocalContactId.equals(mDeleteDetailContact.localContactID)) { final ContactDetail detail = new ContactDetail(); detail.localDetailID = info.mLocalDetailId; detail.key = info.mServerDetailKey; detail.unique_id = info.mServerDetailId; mDeleteDetailContact.details.add(detail); mContactChangeInfoList.add(info); } } /** Debug output. **/ if (Settings.ENABLED_CONTACTS_SYNC_TRACE) { LogUtils.logI("UploadServerContacts.sendNextDeleteDetailsPage() " + "Contact details for deleting:"); for (ContactDetail detail : mDeleteDetailContact.details) { LogUtils.logI("UploadServerContacts." + "sendNextDeleteDetailsPage() Contact ID: " + mDeleteDetailContact.contactID + ", detail = " + detail.key + ", unique_id = " + detail.unique_id); } } /** Debug output. **/ if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) { complete(NetworkAgent.getServiceStatusfromDisconnectReason()); return; } mNoOfItemsSent = mDeleteDetailContact.details.size(); setReqId(Contacts.deleteContactDetails(getEngine(), mDeleteDetailContact.contactID, mDeleteDetailContact.details)); } /** * Sends next add contact/group relation request to the server. Many * contacts can be added by a single request. */ private void sendNextAddGroupRelationsPage() { ContactChangeInfo info = null; mActiveGroupId = null; mContactIdList.clear(); mContactChangeInfoList.clear(); List<ContactChangeInfo> groupInfoList = new ArrayList<ContactChangeInfo>(); long startTime = System.nanoTime(); if (!ContactChangeLogTable.fetchContactChangeLog(groupInfoList, ContactChangeType.ADD_GROUP_REL, 0, MAX_UP_PAGE_SIZE, mDb.getReadableDatabase())) { LogUtils.logE("UploadServerContacts." + "sendNextAddGroupRelationsPage() Unable to fetch add " + "group relations from database"); complete(ServiceStatus.ERROR_DATABASE_CORRUPT); return; } mDbSyncTime += (System.nanoTime() - startTime); if (groupInfoList.size() == 0) { moveToNextState(); return; } mContactChangeInfoList.clear(); List<ContactChangeInfo> deleteInfoList = new ArrayList<ContactChangeInfo>(); for (int i = 0; i < groupInfoList.size(); i++) { info = groupInfoList.get(i); if (info.mServerContactId == null) { info.mServerContactId = mDb.fetchServerId(info.mLocalContactId); } if (info.mServerContactId != null && info.mGroupOrRelId != null) { if (mActiveGroupId == null) { mActiveGroupId = info.mGroupOrRelId; } if (info.mGroupOrRelId.equals(mActiveGroupId)) { mContactIdList.add(info.mServerContactId); mContactChangeInfoList.add(info); } continue; } LogUtils.logE("UploadServerContact.sendNextAddGroupRelationsPage() " + "Invalid add group change: SID = " + info.mServerContactId + ", gid=" + info.mGroupOrRelId); deleteInfoList.add(info); } mDb.deleteContactChanges(deleteInfoList); if (mActiveGroupId == null) { moveToNextState(); return; } mGroupList.clear(); GroupItem group = new GroupItem(); group.mId = mActiveGroupId; mGroupList.add(group); if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) { complete(NetworkAgent.getServiceStatusfromDisconnectReason()); return; } mNoOfItemsSent = mGroupList.size(); setReqId(GroupPrivacy.addContactGroupRelations(getEngine(), mContactIdList, mGroupList)); } /** * Sends next delete contact/group relation request to the server. */ private void sendNextDelGroupRelationsPage() { ContactChangeInfo info = null; mActiveGroupId = null; mContactChangeInfoList.clear(); List<ContactChangeInfo> groupInfoList = new ArrayList<ContactChangeInfo>(); long startTime = System.nanoTime(); if (!ContactChangeLogTable.fetchContactChangeLog(groupInfoList, ContactChangeType.DELETE_GROUP_REL, 0, MAX_UP_PAGE_SIZE, mDb.getReadableDatabase())) { LogUtils.logE("UploadServerContacts." + "sendNextDelGroupRelationsPage() Unable to fetch delete " + "group relations from database"); complete(ServiceStatus.ERROR_DATABASE_CORRUPT); return; } mDbSyncTime += (System.nanoTime() - startTime); if (groupInfoList.size() == 0) { moveToNextState(); return; } mContactChangeInfoList.clear(); List<ContactChangeInfo> deleteInfoList = new ArrayList<ContactChangeInfo>(); for (int i = 0; i < groupInfoList.size(); i++) { info = groupInfoList.get(i); if (info.mServerContactId == null) { info.mServerContactId = mDb.fetchServerId(info.mLocalContactId); } if (info.mServerContactId != null && info.mGroupOrRelId != null) { if (mActiveGroupId == null) { mActiveGroupId = info.mGroupOrRelId; } if (mActiveGroupId.equals(info.mGroupOrRelId)) { mContactIdList.add(info.mServerContactId); mContactChangeInfoList.add(info); } continue; } LogUtils.logE("UploadServerContact.sendNextDelGroupRelationsPage() " + "Invalid delete group change: SID = " + info.mServerContactId + ", gid=" + info.mGroupOrRelId); deleteInfoList.add(info); } mDb.deleteContactChanges(deleteInfoList); if (mActiveGroupId == null) { moveToNextState(); return; } if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) { complete(NetworkAgent.getServiceStatusfromDisconnectReason()); return; } mNoOfItemsSent = mContactIdList.size(); setReqId(GroupPrivacy.deleteContactGroupRelationsExt(getEngine(), mActiveGroupId, mContactIdList)); } /** * Changes the state of the processor to the next internal state. Is called * when the current state has finished. */ private void moveToNextState() { switch (mInternalState) { case PROCESSING_NEW_CONTACTS: startProcessModifiedDetails(); break; case PROCESSING_MODIFIED_DETAILS: startProcessDeletedContacts(); break; case PROCESSING_DELETED_CONTACTS: startProcessDeletedDetails(); break; case PROCESSING_DELETED_DETAILS: startProcessGroupAdditions(); break; case PROCESSING_GROUP_ADDITIONS: startProcessGroupDeletions(); break; case PROCESSING_GROUP_DELETIONS: LogUtils.logV("UploadServerContacts.moveToNextState() " + "Total DB access time = " + (mDbSyncTime / NANOSECONDS_IN_MS) + "ms, no of changes = " + mTotalNoOfItems); complete(ServiceStatus.SUCCESS); break; default: complete(ServiceStatus.ERROR_NOT_READY); break; } } /** * Called by framework to cancel contact sync. No implementation required, * the server response will be ignored and the contact sync will be repeated * if necessary. */ @Override protected void doCancel() { // Do nothing. } /** * Called by framework when a response to a server request is received. * Processes response based on the current internal state. * * @param resp Response data */ @Override public final void processCommsResponse(final DecodedResponse resp) { switch (mInternalState) { case PROCESSING_NEW_CONTACTS: processNewContactsResp(resp); break; case PROCESSING_MODIFIED_DETAILS: processModifiedDetailsResp(resp); break; case PROCESSING_DELETED_CONTACTS: processDeletedContactsResp(resp); break; case PROCESSING_DELETED_DETAILS: processDeletedDetailsResp(resp); break; case PROCESSING_GROUP_ADDITIONS: processGroupAdditionsResp(resp); break; case PROCESSING_GROUP_DELETIONS: processGroupDeletionsResp(resp); break; default: // Do nothing. break; } } /** * Called when a server response is received during a new contact sync. The * server ID, user ID and contact detail unique IDs are extracted from the * response and the NowPlus database updated. Possibly server errors are * also handled. * * @param resp Response from server. */ private void processNewContactsResp(final DecodedResponse resp) { ServiceStatus status = BaseEngine.getResponseStatus(BaseDataType.CONTACT_CHANGES_DATA_TYPE, resp.mDataTypes); if (status == ServiceStatus.SUCCESS) { ContactChanges contactChanges = (ContactChanges)resp.mDataTypes.get(0); ListIterator<Contact> itContactSrc = contactChanges.mContacts.listIterator(); ListIterator<Contact> itContactDest = mContactChangeList.listIterator(); List<ServerIdInfo> contactServerIdList = new ArrayList<ServerIdInfo>(); List<ServerIdInfo> detailServerIdList = new ArrayList<ServerIdInfo>(); while (itContactSrc.hasNext()) { if (!itContactDest.hasNext()) { /** * The response should contain the same number of contacts * as was supplied but must handle the error. */ status = ServiceStatus.ERROR_COMMS_BAD_RESPONSE; break; } Contact contactSrc = itContactSrc.next(); Contact contactDest = itContactDest.next(); if (Settings.ENABLED_CONTACTS_SYNC_TRACE) { String name = null; String sns = null; for (ContactDetail detail : contactDest.details) { if (detail.key == ContactDetail.DetailKeys.VCARD_NAME) { if (detail.value != null) { VCardHelper.Name nameObj = detail.getName(); if (nameObj != null) { name = nameObj.toString(); } } } if (detail.key == ContactDetail.DetailKeys.VCARD_INTERNET_ADDRESS) { sns = detail.alt; } } LogUtils.logV("UploadServerContacts." + "processNewContactsResp() Contact uploaded: SID" + " = " + contactSrc.contactID + ", name = " + name + ", sns = " + sns + ", no of details = " + contactDest.details.size() + ", deleted=" + contactSrc.deleted); } if (contactSrc.contactID != null && contactSrc.contactID.longValue() != -1L) { if (contactDest.contactID == null || !contactDest.contactID.equals(contactSrc.contactID)) { ServerIdInfo info = new ServerIdInfo(); info.localId = contactDest.localContactID; info.serverId = contactSrc.contactID; info.userId = contactSrc.userID; contactServerIdList.add(info); } } else { LogUtils.logE("UploadServerContacts." + "processNewContactsResp() The server failed to " + "add the following contact: " + contactDest.localContactID + ", server ID = " + contactDest.contactID); mFailureList += "Failed to add contact: " + contactDest.localContactID + "\n"; for (ContactDetail d : contactDest.details) { LogUtils.logV("Failed Contact Info: " + contactDest.localContactID + ", Detail: " + d.key + ", " + d.keyType + " = " + d.value); } } status = handleUploadDetailChanges(contactSrc, contactDest, detailServerIdList); } if (status != ServiceStatus.SUCCESS) { /** Something is going wrong - cancel the update **/ complete(status); return; } long startTime = System.nanoTime(); List<ContactIdInfo> dupList = new ArrayList<ContactIdInfo>(); status = ContactsTable.syncSetServerIds(contactServerIdList, dupList, mDb .getWritableDatabase()); if (status != ServiceStatus.SUCCESS) { complete(status); return; } status = ContactDetailsTable.syncSetServerIds(detailServerIdList, mDb .getWritableDatabase()); if (status != ServiceStatus.SUCCESS) { complete(status); return; } if (dupList.size() > 0) { LogUtils.logV("UploadServerContacts.processNewContactsResp() Found " +dupList.size()+ " duplicate contacts. Trying to remove them..."); if(VersionUtils.is2XPlatform()) { // This is a very important distinction for 2.X devices! // the NAB IDs from the contacts we first import are stripped away // So we won't have the correct ID if syncMergeContactList() is executed // This is critical because a chain reaction will cause a Contact Delete in the end // Instead we can syncDeleteContactList() which should be safe on 2.X! status = mDb.syncDeleteContactList(dupList, false, true); } else { status = mDb.syncMergeContactList(dupList); } if (status != ServiceStatus.SUCCESS) { complete(status); return; } markDbChanged(); } mDbSyncTime += (System.nanoTime() - startTime); while (itContactDest.hasNext()) { Contact contactDest = itContactDest.next(); LogUtils.logE("UploadServerContacts.processNewContactsResp() " + "The server failed to add the following contact (not " + "included in returned list): " + contactDest.localContactID); mFailureList += "Failed to add contact (missing from return " + "list): " + contactDest.localContactID + "\n"; } updateProgress(); sendNextContactAdditionsPage(); return; } complete(status); } /** * Called when a server response is received during a modified contact sync. * The server ID, user ID and contact detail unique IDs are extracted from * the response and the NowPlus database updated if necessary. Possibly * server errors are also handled. * * @param resp Response from server. */ private void processModifiedDetailsResp(final DecodedResponse resp) { ServiceStatus status = BaseEngine.getResponseStatus(BaseDataType.CONTACT_CHANGES_DATA_TYPE, resp.mDataTypes); if (status == ServiceStatus.SUCCESS) { ContactChanges contactChanges = (ContactChanges)resp.mDataTypes.get(0); ListIterator<Contact> itContactSrc = contactChanges.mContacts.listIterator(); ListIterator<Contact> itContactDest = mContactChangeList.listIterator(); List<ServerIdInfo> detailServerIdList = new ArrayList<ServerIdInfo>(); while (itContactSrc.hasNext()) { if (!itContactDest.hasNext()) { /* * The response should contain the same number of contacts * as was supplied but must handle the error. */ status = ServiceStatus.ERROR_COMMS_BAD_RESPONSE; break; } status = handleUploadDetailChanges(itContactSrc.next(), itContactDest.next(), detailServerIdList); } if (status != ServiceStatus.SUCCESS) { /** Something is going wrong - cancel the update. **/ complete(status); return; } long startTime = System.nanoTime(); status = ContactDetailsTable.syncSetServerIds(detailServerIdList, mDb .getWritableDatabase()); if (status != ServiceStatus.SUCCESS) { complete(status); return; } mDb.deleteContactChanges(mContactChangeInfoList); mDbSyncTime += (System.nanoTime() - startTime); mContactChangeInfoList.clear(); updateProgress(); sendNextDetailChangesPage(); return; } LogUtils.logE("UploadServerContacts.processModifiedDetailsResp() " + "Error requesting contact changes, error = " + status); complete(status); } /** * Called when a server response is received during a deleted contact sync. * The server change log is updated. Possibly server errors are also * handled. * * @param resp Response from server. */ private void processDeletedContactsResp(final DecodedResponse resp) { ServiceStatus status = BaseEngine.getResponseStatus(BaseDataType.CONTACT_LIST_RESPONSE_DATA_TYPE, resp.mDataTypes); if (status == ServiceStatus.SUCCESS) { ContactListResponse result = (ContactListResponse)resp.mDataTypes.get(0); ListIterator<ContactChangeInfo> infoIt = mContactChangeInfoList.listIterator(); for (Integer contactID : result.mContactIdList) { if (!infoIt.hasNext()) { complete(ServiceStatus.ERROR_COMMS_BAD_RESPONSE); return; } ContactChangeInfo info = infoIt.next(); if (contactID == null || contactID.intValue() == -1) { LogUtils.logE("UploadServerContacts." + "processDeletedContactsResp() The server failed " + "to delete the following contact: LocalId = " + info.mLocalContactId + ", ServerId = " + info.mServerContactId); mFailureList += "Failed to delete contact: " + info.mLocalContactId + "\n"; } } long startTime = System.nanoTime(); mDb.deleteContactChanges(mContactChangeInfoList); mDbSyncTime += (System.nanoTime() - startTime); mContactChangeInfoList.clear(); updateProgress(); sendNextDeleteContactsPage(); return; } LogUtils.logE("UploadServerContacts.processModifiedDetailsResp() " + "Error requesting contact changes, error = " + status); complete(status); } /** * Called when a server response is received during a deleted contact detail * sync. The server change log is updated. Possibly server errors are also * handled. * * @param resp Response from server. */ private void processDeletedDetailsResp(final DecodedResponse resp) { ServiceStatus status = BaseEngine.getResponseStatus(BaseDataType.CONTACT_DETAIL_DELETION_DATA_TYPE, resp.mDataTypes); if (status == ServiceStatus.SUCCESS) { ContactDetailDeletion result = (ContactDetailDeletion)resp.mDataTypes.get(0); if (result.mDetails != null) { LogUtils.logV("UploadServerContacts." + "processDeletedDetailsResp() Deleted details " + result.mDetails.size()); } ListIterator<ContactChangeInfo> infoIt = mContactChangeInfoList.listIterator(); if (result.mContactId == null || result.mContactId == -1) { boolean first = true; while (infoIt.hasNext()) { ContactChangeInfo info = infoIt.next(); if (first) { first = false; LogUtils.logE("UploadServerContacts." + "processDeletedDetailsResp() The server " + "failed to delete detail from the following " + "contact: LocalId = " + info.mLocalContactId + ", ServerId = " + info.mServerContactId); } mFailureList += "Failed to delete detail: " + info.mLocalDetailId + "\n"; } } else if (result.mDetails != null) { for (ContactDetail d : result.mDetails) { if (!infoIt.hasNext()) { complete(ServiceStatus.ERROR_COMMS_BAD_RESPONSE); return; } ContactChangeInfo info = infoIt.next(); if (!d.key.equals(info.mServerDetailKey)) { LogUtils.logE("UploadServerContacts." + "processDeletedDetailsResp() The server " + "failed to delete the following detail: " + "LocalId = " + info.mLocalContactId + ", " + "ServerId = " + info.mServerContactId + ", key = " + info.mServerDetailKey + ", detail ID = " + info.mServerDetailId); mFailureList += "Failed to delete detail: " + info.mLocalDetailId + "\n"; } } } long startTime = System.nanoTime(); mDb.deleteContactChanges(mContactChangeInfoList); mDbSyncTime += (System.nanoTime() - startTime); mContactChangeInfoList.clear(); updateProgress(); sendNextDeleteDetailsPage(); return; } LogUtils.logE("UploadServerContacts.processModifiedDetailsResp() " + "Error requesting contact changes, error = " + status); complete(status); } /** * Called when a server response is received during a group/contact add * relation sync. The server change log is updated. Possibly server errors * are also handled. * * @param resp Response from server. */ private void processGroupAdditionsResp(final DecodedResponse resp) { ServiceStatus status = BaseEngine.getResponseStatus(BaseDataType.ITEM_LIST_DATA_TYPE, resp.mDataTypes); if (status == ServiceStatus.SUCCESS) { if (resp.mDataTypes.size() == 0) { LogUtils.logE("UploadServerContacts." + "processGroupAdditionsResp() " + "Item list cannot be empty"); complete(ServiceStatus.ERROR_COMMS_BAD_RESPONSE); return; } ItemList itemList = (ItemList)resp.mDataTypes.get(0); if (itemList.mItemList == null) { LogUtils.logE("UploadServerContacts." + "processGroupAdditionsResp() " + "Item list cannot be NULL"); complete(ServiceStatus.ERROR_COMMS_BAD_RESPONSE); return; } // TODO: Check response long startTime = System.nanoTime(); mDb.deleteContactChanges(mContactChangeInfoList); mDbSyncTime += (System.nanoTime() - startTime); mContactChangeInfoList.clear(); updateProgress(); sendNextAddGroupRelationsPage(); return; } LogUtils.logE("UploadServerContacts.processGroupAdditionsResp() " + "Error adding group relations, error = " + status); complete(status); } /** * Called when a server response is received during a group/contact delete * relation sync. The server change log is updated. Possibly server errors * are also handled. * * @param resp Response from server. */ private void processGroupDeletionsResp(final DecodedResponse resp) { ServiceStatus status = BaseEngine.getResponseStatus(BaseDataType.STATUS_MSG_DATA_TYPE, resp.mDataTypes); if (status == ServiceStatus.SUCCESS) { if (resp.mDataTypes.size() == 0) { LogUtils.logE("UploadServerContacts." + "processGroupDeletionsResp() " + "Response cannot be empty"); complete(ServiceStatus.ERROR_COMMS_BAD_RESPONSE); return; } StatusMsg result = (StatusMsg)resp.mDataTypes.get(0); if (!result.mStatus.booleanValue()) { LogUtils.logE("UploadServerContacts." + "processGroupDeletionsResp() Error deleting group " + "relation, error = " + result.mError); complete(ServiceStatus.ERROR_COMMS_BAD_RESPONSE); } LogUtils.logV("UploadServerContacts." + "processGroupDeletionsResp() Deleted relation"); long startTime = System.nanoTime(); mDb.deleteContactChanges(mContactChangeInfoList); mDbSyncTime += (System.nanoTime() - startTime); mContactChangeInfoList.clear(); updateProgress(); sendNextDelGroupRelationsPage(); return; } LogUtils.logE("UploadServerContacts.processGroupDeletionsResp() " + "Error deleting group relation, error = " + status); complete(status); } /** * Called when handling the server response from a new contact or modify * contact sync. Updates the unique IDs for all the details if necessary. * * @param contactSrc Contact received from server. * @param contactDest Contact from database. * @param detailServerIdList List of contact details with updated unique id. * @return ServiceStatus object. */ private ServiceStatus handleUploadDetailChanges(final Contact contactSrc, final Contact contactDest, final List<ServerIdInfo> detailServerIdList) { if (contactSrc.contactID == null || contactSrc.contactID.longValue() == -1L) { LogUtils.logE("UploadServerContacts.handleUploadDetailChanges() " + "The server failed to modify the following contact: " + contactDest.localContactID); mFailureList += "Failed to add contact: " + contactDest.localContactID + "\n"; return ServiceStatus.SUCCESS; } ListIterator<ContactDetail> itContactDetailSrc = contactSrc.details.listIterator(); ListIterator<ContactDetail> itContactDetailDest = contactDest.details.listIterator(); while (itContactDetailSrc.hasNext()) { if (!itContactDetailDest.hasNext()) { /* * The response should contain the same number of details as was * supplied but must handle the error. */ return ServiceStatus.ERROR_COMMS_BAD_RESPONSE; } ContactDetail contactDetailSrc = itContactDetailSrc.next(); ContactDetail contactDetailDest = itContactDetailDest.next(); ServerIdInfo info = new ServerIdInfo(); info.localId = contactDetailDest.localDetailID; if (contactDetailSrc.unique_id != null && contactDetailSrc.unique_id.longValue() == -1L) { LogUtils.logE("UploadServerContacts." + "handleUploadDetailChanges() The server failed to " + "modify the following contact detail: LocalDetailId " + "= " + contactDetailDest.localDetailID + ", Key = " + contactDetailDest.key + ", value = " + contactDetailDest.value); mFailureList += "Failed to modify contact detail: " + contactDetailDest.localDetailID + ", for contact " + contactDetailDest.localContactID + "\n"; info.serverId = null; } else { info.serverId = contactDetailSrc.unique_id; } detailServerIdList.add(info); } while (itContactDetailDest.hasNext()) { ContactDetail contactDetailDest = itContactDetailDest.next(); mFailureList += "Failed to modify contact detail (not in return " + "list):" + contactDetailDest.localDetailID + ", for contact " + contactDetailDest.localContactID + "\n"; LogUtils.logE("UploadServerContacts.handleUploadDetailChanges() " + "The server failed to modify the following contact detail " + "(not found in returned list): LocalDetailId = " + contactDetailDest.localDetailID + ", Key = " + contactDetailDest.key + ", value = " + contactDetailDest.value); } return ServiceStatus.SUCCESS; } /** * Helper method to update the progress UI. */ private void updateProgress() { mItemsDone += mNoOfItemsSent; if (mTotalNoOfItems > 0) { setSyncStatus(new SyncStatus((int)(mItemsDone * MAX_PROGESS / mTotalNoOfItems), "", Task.UPDATE_SERVER_CONTACTS, TaskStatus.SENT_CONTACTS, mItemsDone, mTotalNoOfItems)); } else { setSyncStatus(new SyncStatus(0, "", SyncStatus.Task.UPDATE_SERVER_CONTACTS)); } } /** * Returns the current internal state of the processor (for testing only). * * @return InternalState of processor. */ public final InternalState getInternalState() { return mInternalState; } /** * Returns a list of server IDs which is sent to the server by the {@code * DeleteContacts} API (for testing only). * * @return List of server IDs. */ public final List<Long> getContactIdList() { return mContactIdList; } /** * Return the current list of groups being added to the contacts listed in * the {@link #mContactIdList}. Will only contain a single group (list is * needed for the * {@link GroupPrivacy#addContactGroupRelations(BaseEngine, List, List)} * API) (for testing only). * * @return List of groups. */ public final List<GroupItem> getGroupList() { return mGroupList; } /** * Returns the next page of contacts being sent to the server which have * either been added or changed (for testing only). * * @return List of contacts. */ public final List<Contact> getContactChangeList() { return mContactChangeList; } /** * Returns the main contact during the contact detail deletion process (for * testing only). * * @return Main contact. */ public final Contact getDeleteDetailContact() { return mDeleteDetailContact; } /** * Returns the server ID of the group being associated with a contact (for * testing only). * * @return Server ID. */ public final Long getActiveGroupId() { return mActiveGroupId; } @Override protected void complete(ServiceStatus status) { // do our internal cleanup before completing if (mContactsCursor != null) { mContactsCursor.close(); mContactsCursor = null; } // call the base class implementation super.complete(status); } }