/*
* 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;
}
}