/* * CDDL HEADER START * * The contents of this file are subject to the terms of the Common Development * and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at * src/com/vodafone360/people/VODAFONE.LICENSE.txt or * http://github.com/360/360-Engine-for-Android * See the License for the specific language governing permissions and * limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each file and * include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the fields * enclosed by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved. * Use is subject to license terms. */ package com.vodafone360.people.engine.contactsync; import java.util.ArrayList; import java.util.List; import android.database.sqlite.SQLiteDatabase; import com.vodafone360.people.database.DatabaseHelper; import com.vodafone360.people.database.DatabaseHelper.DatabaseChangeType; import com.vodafone360.people.database.tables.ContactDetailsTable; import com.vodafone360.people.database.tables.ContactSummaryTable; import com.vodafone360.people.database.tables.ContactsTable; import com.vodafone360.people.database.tables.NativeChangeLogTable; import com.vodafone360.people.database.tables.ContactsTable.ContactIdInfo; import com.vodafone360.people.database.tables.NativeChangeLogTable.ContactChangeType; import com.vodafone360.people.datatypes.ContactDetail; import com.vodafone360.people.service.ServiceStatus; import com.vodafone360.people.utils.LogUtils; /** * The PeopleContactsApi wrapper class of the People contacts database. * * Modifying the People database by adding, modifying and deleting contacts * should be done via this class to ensure that the database remain consistent * across all the tables. * * Note: this class is an attempt to separate the internal People contacts persistence from * other components that need to access it (i.e. hiding its database, SQL tables and internals). * It is not yet used by all the code base that would need to. */ public class PeopleContactsApi { /** * Handler to the DatabaseHelper class. */ private DatabaseHelper mDbh; /** * Array of ContactIdInfo. * * Used for compatibility reasons with DatabaseHelper method and kept as a class member to avoid * frequent allocation. * @see DatabaseHelper#syncDeleteContactList(List, boolean, boolean) */ private List<ContactsTable.ContactIdInfo> mContactIdInfoList = new ArrayList<ContactsTable.ContactIdInfo>(1); /** * Array of added ContactDetail. * * Used for compatibility reasons with DatabaseHelper method and kept as a class member to avoid * frequent allocation. * @see DatabaseHelper#syncAddContactDetailList(List, boolean, boolean) */ private ArrayList<ContactDetail> mAddedDetails = new ArrayList<ContactDetail>(); /** * Array of updated ContactDetail. * * Used for compatibility reasons with DatabaseHelper method and kept as a class member to avoid * frequent allocation. * @see DatabaseHelper#syncModifyContactDetailList(List, boolean, boolean) */ private ArrayList<ContactDetail> mUpdatedDetails = new ArrayList<ContactDetail>(); /** * Array of deleted ContactDetail. * * Used for compatibility reasons with DatabaseHelper method and kept as a class member to avoid * frequent allocation. * @see DatabaseHelper#syncDeletedContactDetailList(List, boolean, boolean) */ private ArrayList<ContactDetail> mDeletedDetails = new ArrayList<ContactDetail>(); /** * Constructor. * * @param dbh the DatabaseHelper to access the people database */ public PeopleContactsApi(DatabaseHelper dbh) { mDbh = dbh; } /** * Gets the list of native contacts ids stored in the people database. * Contacts being deleted but still in the database will also be returned. * * @return an array of native contacts ids */ public long[] getNativeContactsIds() { try { final SQLiteDatabase db = mDbh.getReadableDatabase(); // get the native ids from the Contacts table final long[] existingIds = ContactsTable.getNativeContactsIds(db); // get the deleted native ids form the Change log table final long[] deletedIds = NativeChangeLogTable.getDeletedContactsNativeIds(db); // merge both arrays to get the full list of native ids on People database side return mergeSortedArrays(existingIds, deletedIds); } catch (Exception e) { LogUtils.logE("getNativeContactsIds(), error: "+e); } return null; } /** * Gets an array of contacts people ids that need to be synced back to native. * * @return */ public long[] getNativeSyncableContactIds() { long[] ids = null; try { // get the "syncable to native" contacts ids = mDbh.getNativeSyncableContactsLocalIds(); } catch (Exception e) { LogUtils.logE("getNativeSyncableContactsIds(), error: "+e); } return ids; } /** * Deletes a Contact in the people database from its native id. * * Note: it assumes that the deletion comes from native as it sets flags * to prevent syncing back to native * * @param nativeId the native id of the contact to delete * @param syncToNative true if the deletion has to be propagated to native, false otherwise */ public boolean deleteNativeContact(long nativeId, boolean syncToNative) { try { final SQLiteDatabase readableDb = mDbh.getReadableDatabase(); ContactsTable.ContactIdInfo info = ContactsTable.fetchContactIdFromNative((int)nativeId, readableDb); if (info != null) { mContactIdInfoList.clear(); mContactIdInfoList.add(info); info.nativeId = (int)nativeId; ServiceStatus status = mDbh.syncDeleteContactList(mContactIdInfoList, true, syncToNative); if (ServiceStatus.SUCCESS == status) { // TODO: Throttle the event mDbh.fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, true); return true; } } } catch (Exception e) { LogUtils.logE("deleteNativeContact("+nativeId+"), error: "+e); } return false; } /** * Adds a native contact to the people database. * * Note: it assumes that the new contact comes from native as it sets flags * to prevent syncing back to native * * @param contact the ContactChange array representing the contact to add * @return true if successful, false otherwise */ public boolean addNativeContact(ContactChange[] contact) { final boolean result = mDbh.addNativeContact(contact); // TODO: Throttle the event if (result) mDbh.fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, true); return result; } /** * Updates a native contact in the people database. * * Note: it assumes that the changes come from native as it sets flags * to prevent syncing back to native * * @param contact the contact changes to apply to the contact */ public void updateNativeContact(ContactChange[] contact) { mAddedDetails.clear(); mDeletedDetails.clear(); mUpdatedDetails.clear(); for(int i = 0; i < contact.length; i++) { final ContactChange change = contact[i]; // convert the ContactChange into a ContactDetail final ContactDetail detail = mDbh.convertContactChange(change); final int type = change.getType(); switch(type) { case ContactChange.TYPE_ADD_DETAIL: mAddedDetails.add(detail); break; case ContactChange.TYPE_DELETE_DETAIL: mDeletedDetails.add(detail); break; case ContactChange.TYPE_UPDATE_DETAIL: mUpdatedDetails.add(detail); break; } } if (mAddedDetails.size() > 0) { mDbh.syncAddContactDetailList(mAddedDetails, true, false); } if (mDeletedDetails.size() > 0) { mDbh.syncDeleteContactDetailList(mDeletedDetails, true, false); } if (mUpdatedDetails.size() > 0) { mDbh.syncModifyContactDetailList(mUpdatedDetails, true, false); } // TODO: Throttle the event mDbh.fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, true); } /** * Gets a contact from its native id. * * @param nativeId the native id of the contact * @return an array of ContactChange representing the contact, null if not found */ public ContactChange[] getContact(long nativeId) { try { final SQLiteDatabase readableDb = mDbh.getReadableDatabase(); if (NativeChangeLogTable.isContactChangeInList(nativeId, ContactChangeType.DELETE_CONTACT, readableDb)) { // the contact exists as a deleted contact in the NativeChangeLogTable // return one ContactChange with the deleted contact flag ContactChange[] changes = new ContactChange[1]; changes[0] = new ContactChange(ContactChange.TYPE_DELETE_CONTACT); return changes; } else { // get the corresponding local contact id final ContactIdInfo info = ContactsTable.fetchContactIdFromNative((int)nativeId, readableDb); if (info != null) { // we found the contact on CAB, let's get the details final ContactChange[] existingDetails = ContactDetailsTable.getContactChanges((long)info.localId, false, readableDb); // get also the deleted details if any final ContactChange[] deletedDetails = NativeChangeLogTable.getDeletedDetails((long)info.localId, readableDb); if (existingDetails != null && deletedDetails != null) { // merge both arrays ContactChange[] mergedDetails = new ContactChange[existingDetails.length + deletedDetails.length]; System.arraycopy(existingDetails, 0, mergedDetails, 0, existingDetails.length); System.arraycopy(deletedDetails, 0, mergedDetails, existingDetails.length, deletedDetails.length); return mergedDetails; } else if (existingDetails != null) { return existingDetails; } else { return deletedDetails; } } } } catch (Exception e) { LogUtils.logE("getContact("+nativeId+"), error: "+e); } // no contact found return null; } /** * Gets the syncable changes for a contact (i.e. the details not synced yet to native). * * @param localId the localId of the contact * @return an array of ContactChange that need to be synced to native */ public ContactChange[] getNativeSyncableContactChanges(long localId) { // 3 types of changes: new contact, deleted contact, updated contact (new details / removed details / updated detail) ContactChange[] changes = null; try { final SQLiteDatabase readableDb = mDbh.getReadableDatabase(); long nativeId; if ((nativeId = NativeChangeLogTable.getDeletedContactNativeId(localId, readableDb)) != -1) { // the contact exists as a deleted contact in the NativeChangeLogTable // return one ContactChange with the deleted contact flag changes = new ContactChange[1]; changes[0] = new ContactChange(ContactChange.TYPE_DELETE_CONTACT); changes[0].setNabContactId(nativeId); changes[0].setInternalContactId(localId); } else if ((nativeId = ContactsTable.getNativeContactId(localId, readableDb)) == -1) { // the contact is new on native side changes = ContactDetailsTable.getContactChanges(localId, false, readableDb); if (changes != null) { changes[0].setType(ContactChange.TYPE_ADD_CONTACT); } } else { // the contact needs to be updated final ContactChange[] newOrUpdated = ContactDetailsTable.getContactChanges(localId, true, readableDb); final ContactChange[] deleted = NativeChangeLogTable.getDeletedDetails(localId, readableDb); if (nativeId == ContactChange.INVALID_ID) { LogUtils.logE("getNativeSyncableContactChanges(), the native contact id shall not be invalid!"); } // set the native contact id to the new contact changes (only updated or deleted ones have an id already) setNativeContactId(newOrUpdated, nativeId); if (newOrUpdated != null && deleted != null) { // merge needed changes = new ContactChange[newOrUpdated.length + deleted.length]; System.arraycopy(newOrUpdated, 0, changes, 0, newOrUpdated.length); System.arraycopy(deleted, 0, changes, newOrUpdated.length, deleted.length); } else if (newOrUpdated != null) { changes = newOrUpdated; } else { changes = deleted; } } } catch(Exception e) { LogUtils.logE("getNativeSyncableContactChanges(), error: "+e); } return changes; } /** * Sets the native contact id to the new details. * * @param changes the array of changes where to set missing native contact id * @param nativeContactId the native contact id to set */ private void setNativeContactId(ContactChange[] changes, long nativeContactId) { if (changes != null) { final int count = changes.length; for (int i = 0; i < count; i++) { final ContactChange change = changes[i]; if (change.getNabContactId() == ContactChange.INVALID_ID) { change.setNabContactId(nativeContactId); } } } } /** * Sets the native ids to the people database since the contact has been added to native. * * @param contact the array of ContactChange representing the new contact the contact * @param nativeIds the array of ContactChange containing the native ids for the added contact * @return true if successful, false otherwise */ public boolean syncBackNewNativeContact(ContactChange[] contact, ContactChange[] nativeIds) { // set the native ids to Contacts, ContactsSummary and ContactDetails tables if (nativeIds == null || (nativeIds.length != contact.length + 1)) { // we expect an array containing +1 elements as the first element contains the // new native contact id LogUtils.logE("PeopleContactsApi.syncBackNewNativeContact() - the contact was not created on native side"); return false; } SQLiteDatabase writableDb = null; try { writableDb = mDbh.getWritableDatabase(); writableDb.beginTransaction(); if (nativeIds[0] != null) { // nativeIds[0] shall not be null final long localContactId = nativeIds[0].getInternalContactId(); final long nativeContactId = nativeIds[0].getNabContactId(); if (ContactsTable.setNativeContactId(localContactId, nativeContactId, writableDb)) { if (ContactSummaryTable.setNativeContactId(localContactId, nativeContactId, writableDb)) { final int length = contact.length; for (int i = 0; i < length; i++) { final long localDetailId = contact[i].getInternalDetailId(); // some details have no native ids as unique // in that case, we set them with the native contact id // the +1 is because we have to skip the first ContactChange that is here only for getting the nativeContactId final long nativeDetailId; if (nativeIds[i+1] == null || nativeIds[i+1].getNabDetailId() == ContactChange.INVALID_ID) { if (nativeIds[i+1] == null) { // log the failure LogUtils.logE("PeopleContactsApi.syncBackNewNativeContact() - the following ContactChange was not exported to native: "+contact[i]); } nativeDetailId = nativeContactId; } else { nativeDetailId = nativeIds[i+1].getNabDetailId(); } if (!ContactDetailsTable.setDetailSyncedWithNative(localDetailId, nativeContactId, nativeDetailId, true, writableDb)) { return false; } } writableDb.setTransactionSuccessful(); return true; } } } } catch (Exception e) { LogUtils.logE("PeopleContactsApi.syncBackNewNativeContact() - Error: " + e); } finally { if (writableDb != null) { writableDb.endTransaction(); writableDb = null; } } return false; } /** * Acknowledges the people database that the native side deleted the contact as requested. * * @param deletedContact the ContactChange of the deleted contact */ public boolean syncBackDeletedNativeContact(ContactChange deletedContact) { SQLiteDatabase writableDb = null; try { writableDb = mDbh.getWritableDatabase(); writableDb.beginTransaction(); if (NativeChangeLogTable.removeContactChanges(deletedContact.getInternalContactId(), writableDb)) { // the contact is completely removed on native side, // we can now remove it from the native change log table writableDb.setTransactionSuccessful(); return true; } } catch (Exception e) { LogUtils.logE("PeopleContactsApi.syncBackDeletedNativeContact() - Error: " + e); } finally { if (writableDb != null) { writableDb.endTransaction(); writableDb = null; } } return false; } /** * Sets the native ids to the people database for the added details on native side and * removes the deleted details from the people database. * * @param contact the array of ContactChange representing the updates that where performed on the contact * @param nativeIds the array of ContactChange containing the native ids for the added details * @return true if successful, false otherwise */ public boolean syncBackUpdatedNativeContact(ContactChange[] contact, ContactChange[] nativeIds) { if (nativeIds != null && nativeIds.length != contact.length) { // we expect an array with exactly the same size LogUtils.logE("PeopleContactsApi.syncBackUpdatedNativeContact() - ContactChange arrays do not have the same size!"); return false; } SQLiteDatabase writableDb = null; try { writableDb = mDbh.getWritableDatabase(); writableDb.beginTransaction(); final int length = contact.length; for (int i = 0; i < length; i++) { final ContactChange change = contact[i]; final int type = change.getType(); final long localDetailId = contact[i].getInternalDetailId(); switch(type) { case ContactChange.TYPE_ADD_DETAIL: // some details have no native detail ids because unique or not supported // in that case, we set them with the native contact id final long nativeContactId = contact[0].getNabContactId(); final long nativeDetailId; if (nativeIds == null || nativeIds[i] == null || nativeIds[i].getNabDetailId() == ContactChange.INVALID_ID) { if (nativeIds == null || nativeIds[i] == null) { // log the failure LogUtils.logE("PeopleContactsApi.syncBackUpdatedNativeContact() - the following ContactChange was not exported to native: "+change); } nativeDetailId = nativeContactId; } else { nativeDetailId = nativeIds[i].getNabDetailId(); } if (!ContactDetailsTable.setDetailSyncedWithNative(localDetailId, nativeContactId, nativeDetailId, true, writableDb)) { LogUtils.logE("PeopleContactsApi.syncBackUpdatedNativeContact() - error while adding a detail: "+change); return false; } break; case ContactChange.TYPE_UPDATE_DETAIL: // we set the native ids as -1 because we don't need them in that case (update, not an add that generates new ids) if (!ContactDetailsTable.setDetailSyncedWithNative(localDetailId, -1, -1, false, writableDb)) { LogUtils.logE("PeopleContactsApi.syncBackUpdatedNativeContact() - error while updating a detail: "+change); return false; } break; case ContactChange.TYPE_DELETE_DETAIL: // detail removed on native side, finally remove it's deleted log from people database if (!NativeChangeLogTable.removeContactDetailChanges(localDetailId, writableDb)) { LogUtils.logE("PeopleContactsApi.syncBackUpdatedNativeContact() - error while deleting a detail: "+change); return false; } break; } } writableDb.setTransactionSuccessful(); return true; } catch (Exception e) { LogUtils.logE("PeopleContactsApi.syncBackUpdatedNativeContact() - Error: " + e); } finally { if (writableDb != null) { writableDb.endTransaction(); writableDb = null; } } return false; } /** * Merges two sorted arrays in one sorted array. * * @param array1 the first sorted array * @param array2 the second sorted array * @return a sorted array that contains array1 and array2 */ private long[] mergeSortedArrays(long[] array1, long[] array2) { // easy cases if ((array1 == null) && (array2 == null)) { return null; } else if (array1 == null) { return array2; } else if (array2 == null) { return array1; } // interesting case, perform the merge long[] merged = new long[array1.length + array2.length]; int index1 = 0; int index2 = 0; for (int i = 0; i < merged.length; i++) { if (index1 == array1.length) { System.arraycopy(array2, index2, merged, i, array2.length - index2); break; } else if (index2 == array2.length) { System.arraycopy(array1, index1, merged, i, array1.length - index1); break; } else if (array1[index1] < array2[index2]) { merged[i] = array1[index1++]; } else { merged[i] = array2[index2++]; } } return merged; } }