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.database;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileNotFoundException;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.util.ArrayList;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Timer;
  import java.util.TimerTask;
  
  import android.content.ContentValues;
  import android.content.Context;
  import android.database.Cursor;
  import android.database.SQLException;
  import android.database.sqlite.SQLiteDatabase;
  import android.database.sqlite.SQLiteException;
  import android.database.sqlite.SQLiteOpenHelper;
  import android.database.sqlite.SQLiteStatement;
  import android.os.Handler;
  import android.os.Message;
  import android.text.TextUtils;
  import android.util.Log;
  
  import com.vodafone360.people.MainApplication;
  import com.vodafone360.people.Settings;
  import com.vodafone360.people.database.tables.ActivitiesTable;
  import com.vodafone360.people.database.tables.ContactChangeLogTable;
  import com.vodafone360.people.database.tables.ContactDetailsTable;
  import com.vodafone360.people.database.tables.ContactGroupsTable;
  import com.vodafone360.people.database.tables.ContactSourceTable;
  import com.vodafone360.people.database.tables.ContactSummaryTable;
  import com.vodafone360.people.database.tables.ContactsTable;
  import com.vodafone360.people.database.tables.ConversationsTable;
  import com.vodafone360.people.database.tables.GroupsTable;
  import com.vodafone360.people.database.tables.MePresenceCacheTable;
  import com.vodafone360.people.database.tables.MyIdentitiesCacheTable;
  import com.vodafone360.people.database.tables.NativeChangeLogTable;
  import com.vodafone360.people.database.tables.PresenceTable;
  import com.vodafone360.people.database.tables.StateTable;
  import com.vodafone360.people.database.tables.ActivitiesTable.TimelineSummaryItem;
  import com.vodafone360.people.database.tables.ContactChangeLogTable.ContactChangeInfo;
  import com.vodafone360.people.database.tables.ContactDetailsTable.Field;
  import com.vodafone360.people.datatypes.ActivityItem;
  import com.vodafone360.people.datatypes.Contact;
  import com.vodafone360.people.datatypes.ContactDetail;
  import com.vodafone360.people.datatypes.ContactSummary;
  import com.vodafone360.people.datatypes.LoginDetails;
  import com.vodafone360.people.datatypes.PublicKeyDetails;
  import com.vodafone360.people.datatypes.ContactDetail.DetailKeyTypes;
  import com.vodafone360.people.datatypes.ContactDetail.DetailKeys;
  import com.vodafone360.people.engine.contactsync.ContactChange;
  import com.vodafone360.people.engine.meprofile.SyncMeDbUtils;
  import com.vodafone360.people.engine.presence.PresenceDbUtils;
  import com.vodafone360.people.service.PersistSettings;
  import com.vodafone360.people.service.ServiceStatus;
  import com.vodafone360.people.service.ServiceUiRequest;
  import com.vodafone360.people.service.interfaces.IPeopleService;
  import com.vodafone360.people.utils.CloseUtils;
  import com.vodafone360.people.utils.LogUtils;
  import com.vodafone360.people.utils.StringBufferPool;
  import com.vodafone360.people.utils.ThumbnailUtils;
  import com.vodafone360.people.utils.WidgetUtils;
  
  /**
   * The main interface to the client database.
   * <p>
   * The {@link #DATABASE_VERSION} field must be increased each time any change is
   * made to the database schema. This includes any changes to the table name or
   * fields in table classes and any change to persistent settings.
   * <p>
   * All database functionality should be implemented in one of the table Table or
   * Utility sub classes
   * 
   * @version %I%, %G%
   */
  public class DatabaseHelper extends SQLiteOpenHelper {
  
      private static final String LOG_TAG = Settings.LOG_TAG + "Database";
  
      /**
       * The name of the database file.
       */
      private static final String DATABASE_NAME = "people.db";
  
      /**
       * The name of the presence database file which is in memory.
       */
      public static final String DATABASE_PRESENCE = "presence1_db";
  
      /**
       * Contains the database version. Must be increased each time the schema is
       * changed.
       **/
      private static final int DATABASE_VERSION = 64;
  
      private final List<Handler> mUiEventCallbackList = new ArrayList<Handler>();
  
      private Context mContext;
  
      private boolean mMeProfileAvatarChangedFlag;
  
      private boolean mDbUpgradeRequired = false;
      
      /**
       * Time period in which the sending of database change events to the UI is delayed.
       * During this time period duplicate event types are discarded to avoid clogging the
       * event queue (esp. during first time sync).
       */
      private static final long DATABASE_EVENT_DELAY = 1000; // ms
      
      /**
       * Timer to implement a wait before sending database change events to the UI in
       * order to prevent clogging the queue with duplicate events.
       */
      private final Timer mDbEventTimer = new Timer();
      
      /**
       * SELECT DISTINCT LocalId FROM NativeChangeLog UNION SELECT DISTINCT
       * LocalId FROM ContactDetails WHERE NativeSyncId IS NULL OR NativeSyncId <>
       * -1 ORDER BY 1
       */
      private final static String QUERY_NATIVE_SYNCABLE_CONTACTS_LOCAL_IDS = 
      		NativeChangeLogTable.QUERY_MODIFIED_CONTACTS_LOCAL_IDS_NO_ORDERBY 
      		+ " UNION "
              + ContactDetailsTable.QUERY_NATIVE_SYNCABLE_CONTACTS_LOCAL_IDS
              + " ORDER BY 1";
  
      /**
       * Datatype holding a database change event. This datatype is used to collect unique
       * events for a certain period before sending them to the UI to avoid clogging of the
       * event queue.
       */
      private class DbEventType {
          @Override
          public boolean equals(Object o) {
              boolean isEqual = false;
              
              if (o instanceof DbEventType) {
                  DbEventType event = (DbEventType) o;
                  
                  if (  (event.ordinal == this.ordinal)
                      &&(event.isExternal == this.isExternal)) {
                      isEqual = true;
                  }
              }            
              return isEqual;
          }
          
          int ordinal;
          boolean isExternal;
      }
      
      /**
       * List of database change events which needs to be sent to the UI as soon as the a
       * certain amount of time has passed.
       */
      private final List<DbEventType> mDbEvents = new ArrayList<DbEventType>();
      
      /**
       * Timer task which implements the actualy sending of all stored database change events
       * to the UI.
       */
      private class DbEventTimerTask extends TimerTask {
          public void run() {
              synchronized (mDbEvents) {
                  for (DbEventType event:mDbEvents ) {
                  
                      fireEventToUi(ServiceUiRequest.DATABASE_CHANGED_EVENT, event.ordinal,
                              (event.isExternal ? 1 : 0), null);
                  }
                  mDbEvents.clear();
              }
          }
      };
  
      /**
       * Used for passing server contact IDs around.
       */
      public static class ServerIdInfo {
          public Long localId;
  
          public Long serverId;
  
          public Long userId;
      }
  
      /**
       * Used for passing contact avatar information around.
       * 
       * @see #fetchThumbnailUrls
       */
      public static class ThumbnailInfo {
          public Long localContactId;
  
          public String photoServerUrl;
      }
  
      /**
       * An instance of this enum is passed to database change listeners to define
       * the database change type.
       */
      public static enum DatabaseChangeType {
          CONTACTS,
          ACTIVITIES,
          ME_PROFILE,
          ME_PROFILE_PRESENCE_TEXT
      }
  
      /***
       * Public Constructor.
       * 
       * @param context Android context
       */
      public DatabaseHelper(Context context) {
          super(context, DATABASE_NAME, null, DATABASE_VERSION);
          mContext = context;
  
          /*
           * // Uncomment the next line to reset the database //
           * context.deleteDatabase(DATABASE_NAME); // copyDatabaseToSd();
           */
      }
  
      /**
       * Constructor.
       * 
       * @param context the Context where to create the database
       * @param name the name of the database
       */
      public DatabaseHelper(Context context, String name) {
  
          super(context, name, null, DATABASE_VERSION);
          mContext = context;
      }
  
      /**
       * Called the first time the database is generated to create all tables.
       * 
       * @param db An open SQLite database object
       */
      @Override
      public void onCreate(SQLiteDatabase db) {
          try {
              ContactsTable.create(db);
              ContactDetailsTable.create(db);
              ContactSummaryTable.create(db);
              StateTable.create(db);
              ContactChangeLogTable.create(db);
              NativeChangeLogTable.create(db);
              GroupsTable.create(mContext, db);
              ContactGroupsTable.create(db);
              ContactSourceTable.create(db);
              ActivitiesTable.create(db);
              ConversationsTable.create(db);
          } catch (SQLException e) {
              LogUtils.logE("DatabaseHelper.onCreate() SQLException: Unable to create DB table", e);
          }
      }
  
      /**
       * Called whenever the database is opened.
       * 
       * @param db An open SQLite database object
       */
      @Override
      public void onOpen(SQLiteDatabase db) {
          super.onOpen(db);
          // Adding the creation code for the MePresenceCacheTable here because this older
          // versions of the client do not contain this table
          MePresenceCacheTable.create(db);
          db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
          PresenceTable.create(db);
          MyIdentitiesCacheTable.create(db); // will be created if not existing
      }
  
      /***
       * Delete and then recreate a newer database structure.
       *
       * @param db An open SQLite database object
       * @param oldVersion The current database version on the device
       * @param newVersion The required database version
       */
      @Override
      public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
          try {
          	trace(true, "DatabaseHelper.onUpgrade() Upgrading database version from [" + oldVersion
                      + "] to [" + newVersion + "]");
  
          	mDbUpgradeRequired = true;
          	removeUserData();
          	//Clearing the ApplicationCache.
          	((MainApplication) mContext).getCache().clearCachedData(mContext);
  
          } catch (SQLException e) {
              LogUtils.logE("DatabaseHelper.onUpgrade() SQLException: Unable to upgrade database", e);
          }
      }
  
      /***
       * Deletes the database and then fires a Database Changed Event to the UI.
       */
      private void deleteDatabase() {
          trace(true, "DatabaseHelper.deleteDatabase()");
  		synchronized (this) {
  			//The condition is put so that getReadableDatabse() will only be called at time
  			//of ChangeUser and not at the time of DbUpgrade. Because if we call it when
  			//creating or upgrading the Db, that will lead to "IllegalStateException,
  			//getReadableDatabase() called recursively".
  			if (!mDbUpgradeRequired) {
  				getReadableDatabase().close();
  			}
  			mContext.deleteDatabase(DATABASE_NAME);
  		}
          fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, false);
      }
  
      /***
       * Called when the Application is first started.
       */
      public void start() {
          SQLiteDatabase db = getReadableDatabase();
          if (mDbUpgradeRequired) {
              mDbUpgradeRequired = false;
              db.close();
              db = getReadableDatabase();
          }
  
          mMeProfileAvatarChangedFlag = StateTable.fetchMeProfileAvatarChangedFlag(db);
      }
  
      /***
       * Adds a contact to the database and fires an internal database change
       * event.
       * 
       * @param contact A {@link Contact} object which contains the details to be
       *            added
       * @return SUCCESS or a suitable error code
       * @see #deleteContact(long)
       * @see #addContactDetail(ContactDetail)
       * @see #modifyContactDetail(ContactDetail)
       * @see #deleteContactDetail(long)
       * @see #addContactToGroup(long, long)
       * @see #deleteContactFromGroup(long, long)
       */
      public ServiceStatus addContact(Contact contact) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.addContact() contactID[" + contact.contactID
                      + "] nativeContactId[" + contact.nativeContactId + "]");
          }
          List<Contact> contactList = new ArrayList<Contact>();
          contactList.add(contact);
          ServiceStatus status = syncAddContactList(contactList, true, true);
          if (ServiceStatus.SUCCESS == status) {
              fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, false);
          }
          return status;
      }
  
      /***
       * Deletes a contact from the database and fires an internal database change
       * event.
       * 
       * @param localContactID The local ID of the contact to delete
       * @return SUCCESS or a suitable error code
       * @see #addContact(Contact)
       * @see #addContactDetail(ContactDetail)
       * @see #modifyContactDetail(ContactDetail)
       * @see #deleteContactDetail(long)
       * @see #addContactToGroup(long, long)
       * @see #deleteContactFromGroup(long, long)
       */
      public ServiceStatus deleteContact(long localContactID) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.deleteContact() localContactID[" + localContactID + "]");
          }
  
          if (SyncMeDbUtils.getMeProfileLocalContactId(this) != null
                  && SyncMeDbUtils.getMeProfileLocalContactId(this).longValue() == localContactID) {
              LogUtils.logW("DatabaseHelper.deleteContact() Can not delete the Me profile contact");
              return ServiceStatus.ERROR_NOT_FOUND;
          }
  
          ContactsTable.ContactIdInfo contactIdInfo = ContactsTable.validateContactId(
                  localContactID, getWritableDatabase());
          List<ContactsTable.ContactIdInfo> idList = new ArrayList<ContactsTable.ContactIdInfo>();
          idList.add(contactIdInfo);
          ServiceStatus status = syncDeleteContactList(idList, true, true);
          if (ServiceStatus.SUCCESS == status) {
              fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, false);
          }
          return status;
      }
  
      /***
       * Adds a contact detail to the database and fires an internal database
       * change event.
       * 
       * @param detail A {@link ContactDetail} object which contains the detail to
       *            add
       * @return SUCCESS or a suitable error code
       * @see #modifyContactDetail(ContactDetail)
       * @see #deleteContactDetail(long)
       * @see #addContact(Contact)
       * @see #deleteContact(long)
       * @see #addContactToGroup(long, long)
       * @see #deleteContactFromGroup(long, long)
       * @throws NullPointerException When detail is NULL
       */
      public ServiceStatus addContactDetail(ContactDetail detail) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.addContactDetail() name[" + detail.getName() + "]");
          }
          if (detail == null) {
              throw new NullPointerException(
                      "DatabaseHelper.addContactDetail() detail should not be NULL");
          }
          boolean isMeProfile = (SyncMeDbUtils.getMeProfileLocalContactId(this) != null
                  && detail.localContactID != null && detail.localContactID.equals(SyncMeDbUtils
                  .getMeProfileLocalContactId(this)));
  
          List<ContactDetail> detailList = new ArrayList<ContactDetail>();
          detailList.add(detail);
          ServiceStatus status = syncAddContactDetailList(detailList, !isMeProfile, !isMeProfile);
          if (status == ServiceStatus.SUCCESS) {
              fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, false);
              if (isMeProfile) {
                  WidgetUtils.kickWidgetUpdateNow(mContext);
              }
          }
          return status;
      }
  
      /***
       * Modifies an existing contact detail in the database. Also fires an
       * internal database change event.
       * 
       * @param detail A {@link ContactDetail} object which contains the detail to add    
       *         
       * @return SUCCESS or a suitable error code
       * @see #addContactDetail(ContactDetail)
       * @see #deleteContactDetail(long)
       * @see #addContact(Contact)
       * @see #deleteContact(long)
       * @see #addContactToGroup(long, long)
       * @see #deleteContactFromGroup(long, long)
       */
      public ServiceStatus modifyContactDetail(ContactDetail detail) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.modifyContactDetail() name[" + detail.getName() + "]");
          }
          boolean isMeProfile = SyncMeDbUtils.isMeProfile(this, detail.localContactID);
          List<ContactDetail> detailList = new ArrayList<ContactDetail>();
          detailList.add(detail);
          ServiceStatus status = syncModifyContactDetailList(detailList, !isMeProfile, !isMeProfile);
          if (ServiceStatus.SUCCESS == status) {
              fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, false);
              if (isMeProfile) {
                  WidgetUtils.kickWidgetUpdateNow(mContext);
              }
          }
          return status;
      }
  
      /***
       * Deletes a contact detail from the database. Also fires an internal
       * database change event.
       * 
       * @param localContactDetailID The local ID of the detail to delete
       * @return SUCCESS or a suitable error code
       * @see #addContactDetail(ContactDetail)
       * @see #modifyContactDetail(ContactDetail)
       * @see #addContact(Contact)
       * @see #deleteContact(long)
       * @see #addContactToGroup(long, long)
       * @see #deleteContactFromGroup(long, long)
       */
      public ServiceStatus deleteContactDetail(long localContactDetailID) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.deleteContactDetail() localContactDetailID["
                      + localContactDetailID + "]");
          }
          SQLiteDatabase db = getReadableDatabase();
          ContactDetail detail = ContactDetailsTable.fetchDetail(localContactDetailID, db);
          if (detail == null) {
              LogUtils.logE("Database.deleteContactDetail() Unable to find detail for deletion");
              return ServiceStatus.ERROR_NOT_FOUND;
          }
          boolean isMeProfile = false;
          if (detail.localContactID.equals(SyncMeDbUtils.getMeProfileLocalContactId(this))) {
              isMeProfile = true;
          }
          List<ContactDetail> detailList = new ArrayList<ContactDetail>();
          detailList.add(detail);
          ServiceStatus status = syncDeleteContactDetailList(detailList, true, !isMeProfile);
          if (ServiceStatus.SUCCESS == status) {
              fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, false);
              if (isMeProfile) {
                  WidgetUtils.kickWidgetUpdateNow(mContext);
              }
          }
          return status;
      }
  
      /***
       * Modifies the server Contact Id and User ID stored in the database for a
       * specific contact.
       * 
       * @param localId The local Id of the contact to modify
       * @param serverId The new server Id
       * @param userId The new user Id
       * @return true if successful
       * @see #fetchContactByServerId(Long, Contact)
       * @see #fetchServerId(long)
       */
      public boolean modifyContactServerId(long localId, Long serverId, Long userId) {
          trace(false, "DatabaseHelper.modifyContactServerId() localId[" + localId + "] "
                  + "serverId[" + serverId + "] userId[" + userId + "]");
          final SQLiteDatabase db = getWritableDatabase();
  
          try {
  
              db.beginTransaction();
  
              if (!ContactsTable.modifyContactServerId(localId, serverId, userId, db)) {
                  return false;
              }
  
              db.setTransactionSuccessful();
          } finally {
  
              db.endTransaction();
          }
          return true;
      }
  
      /***
       * Sets the Server Id for a contact detail and flags it as synchronized
       * with the server.
       * 
       * @param localDetailId The local Id of the contact detail to modify
       * @param serverDetailId The new server Id
       * @return true if successful
       */
      public boolean syncContactDetail(Long localDetailId, Long serverDetailId) {
          trace(false, "DatabaseHelper.modifyContactDetailServerId() localDetailId[" + localDetailId
                  + "]" + " serverDetailId[" + serverDetailId + "]");
          SQLiteDatabase db = getWritableDatabase();
  
          try {
              db.beginTransaction();
              if (!ContactDetailsTable.syncSetServerId(localDetailId, serverDetailId, db)) {
                  return false;
              }
              db.setTransactionSuccessful();
          } finally {
  
              db.endTransaction();
          }
          return true;
      }
  
      /***
       * Fetches the user's logon credentials from the database.
       * 
       * @param details An empty LoginDetails object which will be filled on
       *            return
       * @return SUCCESS or a suitable error code
       * @see #fetchLogonCredentialsAndPublicKey(LoginDetails, PublicKeyDetails)
       * @see #modifyCredentials(LoginDetails)
       * @see #modifyCredentialsAndPublicKey(LoginDetails, PublicKeyDetails)
       */
      public ServiceStatus fetchLogonCredentials(LoginDetails details) {
          return StateTable.fetchLogonCredentials(details, getReadableDatabase());
      }
  
      /***
       * Fetches the user's logon credentials and public key information from the
       * database.
       * 
       * @param details An empty LoginDetails object which will be filled on
       *            return
       * @param pubKeyDetails An empty PublicKeyDetails object which will be
       *            filled on return
       * @return SUCCESS or a suitable error code
       * @see #fetchLogonCredentials(LoginDetails)
       * @see #modifyCredentials(LoginDetails)
       * @see #modifyCredentialsAndPublicKey(LoginDetails, PublicKeyDetails)
       */
      public ServiceStatus fetchLogonCredentialsAndPublicKey(LoginDetails details,
              PublicKeyDetails pubKeyDetails) {
          return StateTable.fetchLogonCredentialsAndPublicKey(details, pubKeyDetails,
                  getReadableDatabase());
      }
  
      /***
       * Modifies the user's logon credentials. Note: Only called from tests.
       * 
       * @param details The login details to store
       * @return SUCCESS or a suitable error code
       * @see #fetchLogonCredentials(LoginDetails)
       * @see #fetchLogonCredentialsAndPublicKey(LoginDetails, PublicKeyDetails)
       * @see #modifyCredentialsAndPublicKey(LoginDetails, PublicKeyDetails)
       */
      public ServiceStatus modifyCredentials(LoginDetails details) {
          return StateTable.modifyCredentials(details, getWritableDatabase());
      }
  
      /***
       * Modifies the user's logon credentials and public key details.
       * 
       * @param details The login details to store
       * @param pubKeyDetails The public key details to store
       * @return SUCCESS or a suitable error code
       * @see #fetchLogonCredentials(LoginDetails)
       * @see #fetchLogonCredentialsAndPublicKey(LoginDetails, PublicKeyDetails)
       * @see #modifyCredentials(LoginDetails)
       */
      public ServiceStatus modifyCredentialsAndPublicKey(LoginDetails details,
              PublicKeyDetails pubKeyDetails) {
          return StateTable.modifyCredentialsAndPublicKey(details, pubKeyDetails,
                  getWritableDatabase());
      }
  
      /***
       * Remove contact changes from the change log. This will be called once the
       * changes have been sent to the server.
       * 
       * @param changeInfoList A list of changeInfoIDs (none of the other fields
       *            in the {@link ContactChangeInfo} object are required).
       * @return true if successful
       */
      public boolean deleteContactChanges(List<ContactChangeLogTable.ContactChangeInfo> changeInfoList) {
          return ContactChangeLogTable.deleteContactChanges(changeInfoList, getWritableDatabase());
      }
  
      /***
       * Fetches a setting from the database.
       * 
       * @param option The option required.
       * @return A {@link PersistSettings} object which contains the setting data
       *         if successful, null otherwise
       * @see #setOption(PersistSettings)
       */
      public PersistSettings fetchOption(PersistSettings.Option option) {
          PersistSettings setting = StateTable.fetchOption(option, getWritableDatabase());
          if (setting == null) {
              setting = new PersistSettings();
              setting.putDefaultOptionData();
          }
          return setting;
      }
  
      /***
       * Modifies a setting in the database.
       * 
       * @param setting A {@link PersistSetting} object which is populated with an
       *            option set to a value.
       * @return SUCCESS or a suitable error code
       * @see #fetchOption(com.vodafone360.people.service.PersistSettings.Option)
       */
      public ServiceStatus setOption(PersistSettings setting) {
          ServiceStatus status = StateTable.setOption(setting, getWritableDatabase());
          if (ServiceStatus.SUCCESS == status) {
              fireSettingChangedEvent(setting);
          }
          return status;
      }
  
      /***
       * Removes all groups from the database.
       * 
       * @return SUCCESS or a suitable error code
       */
      public ServiceStatus deleteAllGroups() {
          SQLiteDatabase db = getWritableDatabase();
          ServiceStatus status = GroupsTable.deleteAllGroups(db);
          if (ServiceStatus.SUCCESS == status) {
              status = GroupsTable.populateSystemGroups(mContext, db);
          }
          return status;
      }
  
      /***
       * Fetches Avatar URLs from the database for all contacts which have an
       * Avatar and have not yet been loaded.
       * 
       * @param thumbInfoList An empty list where the {@link ThumbnailInfo}
       *            objects will be stored containing the URLs
       * @param firstIndex The 0-based index of the first item to fetch from the
       *            database
       * @param count The maximum number of items to fetch
       * @return SUCCESS or a suitable error code
       * @see ThumbnailInfo
       * @see #fetchThumbnailUrlCount()
       */
      public ServiceStatus fetchThumbnailUrls(List<ThumbnailInfo> thumbInfoList, int firstIndex,
              int count) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.fetchThumbnailUrls() firstIndex[" + firstIndex + "] "
                      + "count[" + count + "]");
          }
          Cursor cursor = null;
          try {
              thumbInfoList.clear();
              cursor = getReadableDatabase().rawQuery(
                      "SELECT " + ContactDetailsTable.TABLE_NAME + "."
                              + ContactDetailsTable.Field.LOCALCONTACTID + "," + Field.STRINGVAL
                              + " FROM " + ContactDetailsTable.TABLE_NAME + " INNER JOIN "
                              + ContactSummaryTable.TABLE_NAME + " WHERE "
                              + ContactDetailsTable.TABLE_NAME + "."
                              + ContactDetailsTable.Field.LOCALCONTACTID + "="
                              + ContactSummaryTable.TABLE_NAME + "."
                              + ContactSummaryTable.Field.LOCALCONTACTID + " AND "
                              + ContactSummaryTable.Field.PICTURELOADED + " =0 " + " AND "
                              + ContactDetailsTable.Field.KEY + "="
                              + ContactDetail.DetailKeys.PHOTO.ordinal() + " LIMIT " + firstIndex
                              + "," + count, null);
  
              ArrayList<String> urls = new ArrayList<String>();
              ThumbnailInfo thumbnailInfo = null;
              while (cursor.moveToNext()) {
                  thumbnailInfo = new ThumbnailInfo();
                  if (!cursor.isNull(0)) {
                      thumbnailInfo.localContactId = cursor.getLong(0);
                  }
                  thumbnailInfo.photoServerUrl = cursor.getString(1);
                  if (!urls.contains(thumbnailInfo.photoServerUrl)) {
                      urls.add(thumbnailInfo.photoServerUrl);
                      thumbInfoList.add(thumbnailInfo);
                  }
              }
              // LogUtils.logWithName("THUMBNAILS:","urls:\n" + urls);
              return ServiceStatus.SUCCESS;
  
          } catch (SQLException e) {
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          } finally {
              CloseUtils.close(cursor);
          }
      }
  
      /***
       * Fetches Avatar URLs from the database for all contacts from contactList
       * which have an Avatar and have not yet been loaded.
       * 
       * @param thumbInfoList An empty list where the {@link ThumbnailInfo}
       *            objects will be stored containing the URLs
       * @param contactList list of contacts to fetch the thumbnails for
       * @return SUCCESS or a suitable error code
       * @see ThumbnailInfo
       * @see #fetchThumbnailUrlCount()
       */
  
      public ServiceStatus fetchThumbnailUrlsForContacts(List<ThumbnailInfo> thumbInfoList,
              final List<Long> contactList) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.fetchThumbnailUrls()");
          }
          StringBuilder localContactIdList = new StringBuilder();
          localContactIdList.append("(");
          Long localContactId = -1l;
          for (Long contactId : contactList) {
              if (localContactId != -1) {
                  localContactIdList.append(",");
              }
              localContactId = contactId;
              localContactIdList.append(contactId);
          }
          localContactIdList.append(")");
          
          Cursor cursor = null;
          try {
              thumbInfoList.clear();
              cursor = getReadableDatabase().rawQuery(
                      "SELECT " + ContactDetailsTable.TABLE_NAME + "."
                              + ContactDetailsTable.Field.LOCALCONTACTID + ","
                              + ContactDetailsTable.Field.STRINGVAL + " FROM "
                              + ContactDetailsTable.TABLE_NAME + " INNER JOIN "
                              + ContactSummaryTable.TABLE_NAME + " WHERE "
                              + ContactDetailsTable.TABLE_NAME + "."
                              + ContactDetailsTable.Field.LOCALCONTACTID + " in "
                              + localContactIdList.toString() + " AND "
                              + ContactSummaryTable.Field.PICTURELOADED + " =0 " + " AND "
                              + ContactDetailsTable.Field.KEY + "="
                              + ContactDetail.DetailKeys.PHOTO.ordinal(), null);
  
              HashSet<String> urlSet = new HashSet<String>();
              ThumbnailInfo thumbnailInfo = null;
              while (cursor.moveToNext()) {
                  thumbnailInfo = new ThumbnailInfo();
                  if (!cursor.isNull(cursor.getColumnIndexOrThrow(
                  		ContactDetailsTable.Field.LOCALCONTACTID.toString()))) {
                      thumbnailInfo.localContactId = cursor.getLong(cursor.getColumnIndexOrThrow(
                      		ContactDetailsTable.Field.LOCALCONTACTID.toString()));
                  }
                  thumbnailInfo.photoServerUrl = cursor.getString(cursor.getColumnIndexOrThrow(
                  		ContactDetailsTable.Field.STRINGVAL.toString()));
                  
                  // TODO: Investigate if this is really needed
                  if (urlSet.add(thumbnailInfo.photoServerUrl)) {
                      thumbInfoList.add(thumbnailInfo);
                  }
              }
  
              return ServiceStatus.SUCCESS;
  
          } catch (SQLException e) {
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          } finally {
              CloseUtils.close(cursor);
          }
      }
      
      /**
       * Fetches the list of all the contactIds for which the Thumbnail still needs to
       * be downloaded. Firstly, the list of all the contactIds whose picture_loaded
       * flag is set to false is retrieved from the ContactSummaryTable. Then these contactids
       * are further filtered based on whether they have a photo URL assigned to them
       * in the ContactDetails table.
       * @param contactIdList An empty list where the retrieved contact IDs are stored.
       * @return SUCCESS or a suitable error code
       */
      public ServiceStatus fetchContactIdsWithThumbnails(List<Long> contactIdList) {
          SQLiteDatabase db = getReadableDatabase();
          Cursor cr = null;
          try {
              String sql = "SELECT " + ContactSummaryTable.Field.LOCALCONTACTID + " FROM "
                      + ContactSummaryTable.TABLE_NAME + " WHERE "
                      + ContactSummaryTable.Field.PICTURELOADED + " =0 AND "
                      + ContactSummaryTable.Field.LOCALCONTACTID + " IN (SELECT "
                      + ContactDetailsTable.Field.LOCALCONTACTID + " FROM "
                      + ContactDetailsTable.TABLE_NAME + " WHERE " + ContactDetailsTable.Field.KEY
                      + "=" + ContactDetail.DetailKeys.PHOTO.ordinal() + ")";
                      
  
              cr = db.rawQuery(sql, null);
              
              Long localContactId = -1L;
              while (cr.moveToNext()) {
                  if (!cr
                          .isNull(cr
                                  .getColumnIndexOrThrow(ContactDetailsTable.Field.LOCALCONTACTID
                                          .toString()))) {
                      localContactId = cr.getLong(cr
                              .getColumnIndexOrThrow(ContactDetailsTable.Field.LOCALCONTACTID
                                      .toString()));
                      contactIdList.add(localContactId);
                  }
              }
              return ServiceStatus.SUCCESS;
          } catch (SQLException e) {
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          } finally {
              CloseUtils.close(cr);
          }
      }
  
      /***
       * Fetches the number of Contact Avatars which have not yet been loaded.
       * 
       * @return The number of Avatars
       * @see ThumbnailInfo
       * @see #fetchThumbnailUrls(List, int, int)
       */
      public int fetchThumbnailUrlCount() {
          trace(false, "DatabaseHelper.fetchThumbnailUrlCount()");
          Cursor cursor = null;
          try {
              cursor = getReadableDatabase().rawQuery(
                      "SELECT COUNT(" + ContactSummaryTable.Field.SUMMARYID + ") FROM "
                              + ContactSummaryTable.TABLE_NAME + " WHERE "
                              + ContactSummaryTable.Field.PICTURELOADED + " =0 ", null);
              if (cursor.moveToFirst()) {
                  if (!cursor.isNull(0)) {
                      return cursor.getInt(0);
                  }
              }
              return 0;
          } catch (SQLException e) {
              return 0;
          } finally {
              CloseUtils.close(cursor);
          }
      }
  
      /***
       * Modifies the Me Profile Avatar Changed Flag. When this flag is set to
       * true, it indicates that the avatar needs to be synchronised with the
       * server.
       * 
       * @param avatarChanged true to set the flag, false to clear the flag
       * @return SUCCESS or a suitable error code
       */
      public ServiceStatus modifyMeProfileAvatarChangedFlag(boolean avatarChanged) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.modifyMeProfileAvatarChangedFlag() avatarChanged["
                      + avatarChanged + "]");
          }
          if (avatarChanged == mMeProfileAvatarChangedFlag) {
              return ServiceStatus.SUCCESS;
          }
          ServiceStatus result = StateTable.modifyMeProfileChangedFlag(avatarChanged,
                  getWritableDatabase());
          if (ServiceStatus.SUCCESS == result) {
              mMeProfileAvatarChangedFlag = avatarChanged;
          }
          return result;
      }
  
      /***
       * Fetches a cursor which can be used to iterate through the main contact
       * list.
       * <p>
       * The ContactSummaryTable.getQueryData static method can be used on the
       * cursor returned by this method to create a ContactSummary object.
       * 
       * @param groupFilterId The local ID of a group to filter, or null if no
       *            filter is required
       * @param constraint A search string to filter the contact name, or null if
       *            no filter is required
       * @return The cursor result
       */
      public synchronized Cursor openContactSummaryCursor(Long groupFilterId, CharSequence constraint) {
          return ContactSummaryTable.openContactSummaryCursor(groupFilterId, constraint,
                  SyncMeDbUtils.getMeProfileLocalContactId(this), getReadableDatabase());
      }
  
      public synchronized Cursor openContactsCursor() {
          return ContactsTable.openContactsCursor(getReadableDatabase());
      }
  
      /***
       * Fetches a contact from the database by its localContactId. The method
       * {@link #fetchBaseContact(long, Contact)} should be used if the contact
       * details properties are not required.
       * 
       * @param localContactId Local ID of the contact to fetch.
       * @param contact Empty {@link Contact} object which will be populated with
       *            data.
       * @return SUCCESS or a suitable ServiceStatus error code.
       */
      public synchronized ServiceStatus fetchContact(long localContactId, Contact contact) {
          SQLiteDatabase db = getReadableDatabase();
          ServiceStatus status = fetchBaseContact(localContactId, contact, db);
          if (ServiceStatus.SUCCESS != status) {
              return status;
          }
          status = ContactDetailsTable.fetchContactDetails(localContactId, contact.details, db);
          if (ServiceStatus.SUCCESS != status) {
              return status;
          }
          return ServiceStatus.SUCCESS;
      }
  
      /***
       * Fetches a contact detail from the database.
       * 
       * @param localDetailId The local ID of the detail to fetch
       * @param detail A empty {@link ContactDetail} object which will be filled
       *            with the data
       * @return SUCCESS or a suitable error code
       */
      public synchronized ServiceStatus fetchContactDetail(long localDetailId, ContactDetail detail) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.fetchContactDetail() localDetailId[" + localDetailId + "]");
          }
          Cursor cursor = null;
          try {
              try {
                  String[] args = {
                      String.format("%d", localDetailId)
                  };
                  cursor = getReadableDatabase()
                          .rawQuery(
                                  ContactDetailsTable
                                          .getQueryStringSql(ContactDetailsTable.Field.DETAILLOCALID
                                                  + " = ?"), args);
              } catch (SQLiteException e) {
                  LogUtils.logE("DatabaseHelper.fetchContactDetail() Unable to fetch contact detail",
                          e);
                  return ServiceStatus.ERROR_DATABASE_CORRUPT;
              }
              if (!cursor.moveToFirst()) {
                  return ServiceStatus.ERROR_NOT_FOUND;
              }
              detail.copy(ContactDetailsTable.getQueryData(cursor));
              return ServiceStatus.SUCCESS;
          } finally {
              CloseUtils.close(cursor);
          }
      }
  
      /***
       * Searches the database for a contact with a given phone number.
       * 
       * @param phoneNumber The telephone number to find
       * @param contact An empty Contact object which will be filled if a contact
       *            is found
       * @param phoneDetail An empty {@link ContactDetail} object which will be
       *            filled with the matching phone number detail
       * @return SUCCESS or a suitable error code
       */
      public synchronized ServiceStatus fetchContactInfo(String phoneNumber, Contact contact,
              ContactDetail phoneDetail) {
          ServiceStatus status = ContactDetailsTable.fetchContactInfo(phoneNumber, phoneDetail,
                  null, getReadableDatabase());
          if (ServiceStatus.SUCCESS != status) {
              return status;
          }
          return fetchContact(phoneDetail.localContactID, contact);
      }
  
      /***
       * Puts a contact into a group.
       * 
       * @param localContactId The local Id of the contact
       * @param groupId The local group Id
       * @return SUCCESS or a suitable error code
       * @see #deleteContactFromGroup(long, long)
       */
      public ServiceStatus addContactToGroup(long localContactId, long groupId) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.addContactToGroup() localContactId[" + localContactId
                      + "] " + "groupId[" + groupId + "]");
          }
          SQLiteDatabase db = getWritableDatabase();
          List<Long> groupIds = new ArrayList<Long>();
          ContactGroupsTable.fetchContactGroups(localContactId, groupIds, db);
          if (groupIds.contains(groupId)) {
              // group is already in db than it's ok
              return ServiceStatus.SUCCESS;
          }
          boolean syncToServer = true;
          boolean isMeProfile = false;
          if (SyncMeDbUtils.getMeProfileLocalContactId(this) != null
                  && SyncMeDbUtils.getMeProfileLocalContactId(this).longValue() == localContactId) {
              isMeProfile = true;
              syncToServer = false;
          }
          Contact contact = new Contact();
          ServiceStatus status = fetchContact(localContactId, contact);
          if (ServiceStatus.SUCCESS != status) {
              return status;
          }
  
          try {
              db.beginTransaction();
  
              if (!ContactGroupsTable.addContactToGroup(localContactId, groupId, db)) {
                  return ServiceStatus.ERROR_DATABASE_CORRUPT;
              }
              if (syncToServer) {
                  if (!ContactChangeLogTable.addGroupRel(localContactId, contact.contactID, groupId,
                          db)) {
                      return ServiceStatus.ERROR_DATABASE_CORRUPT;
                  }
              }
              db.setTransactionSuccessful();
          } finally {
  
              db.endTransaction();
          }
          if (syncToServer && !isMeProfile) {
              fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, false);
          }
          return ServiceStatus.SUCCESS;
      }
  
      /***
       * Removes a group from a contact.
       * 
       * @param localContactId The local Id of the contact
       * @param groupId The local group Id
       * @return SUCCESS or a suitable error code
       * @see #addContactToGroup(long, long)
       */
      public ServiceStatus deleteContactFromGroup(long localContactId, long groupId) {
          if (Settings.ENABLED_DATABASE_TRACE)
              trace(false, "DatabaseHelper.deleteContactFromGroup() localContactId[" + localContactId
                      + "] groupId[" + groupId + "]");
          boolean syncToServer = true;
          boolean meProfile = false;
          if (SyncMeDbUtils.getMeProfileLocalContactId(this) != null
                  && SyncMeDbUtils.getMeProfileLocalContactId(this).longValue() == localContactId) {
              meProfile = true;
              syncToServer = false;
          }
          Contact contact = new Contact();
          ServiceStatus status = fetchContact(localContactId, contact);
          if (ServiceStatus.SUCCESS != status) {
              return status;
          }
          if (contact.contactID == null) {
              return ServiceStatus.ERROR_NOT_READY;
          }
  
          SQLiteDatabase db = getWritableDatabase();
          try {
  
              db.beginTransaction();
              boolean result = ContactGroupsTable.deleteContactFromGroup(localContactId, groupId,
                      db);
              if (result && syncToServer) {
                  if (!ContactChangeLogTable.deleteGroupRel(localContactId, contact.contactID,
                          groupId, db)) {
                      return ServiceStatus.ERROR_DATABASE_CORRUPT;
                  }
              }
              db.setTransactionSuccessful();
          } finally {
  
              db.endTransaction();
          }
          if (syncToServer && !meProfile) {
              fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, false);
          }
          return ServiceStatus.SUCCESS;
      }
  
      /***
       * Removes all the status or timeline activities from the database. Note:
       * Only called from tests.
       * 
       * @param flag The type of activity to delete or null to delete all
       * @return SUCCESS or a suitable error code
       * @see #addActivities(List)
       * @see #fetchActivitiesIds(List, Long)
       */
      public ServiceStatus deleteActivities(Integer flag) {
          if (Settings.ENABLED_DATABASE_TRACE)
              trace(false, "DatabaseHelper.deleteActivities() flag[" + flag + "]");
          ServiceStatus status = ActivitiesTable.deleteActivities(flag, getWritableDatabase());
          if (ServiceStatus.SUCCESS == status) {
              if (flag == null || flag.intValue() == ActivityItem.TIMELINE_ITEM) {
                  StateTable.modifyLatestPhoneCallTime(System.currentTimeMillis(),
                          getWritableDatabase());
              }
          }
          fireDatabaseChangedEvent(DatabaseChangeType.ACTIVITIES, true);
          return status;
      }
  
      /***
       * Removes the selected timeline activity from the database.
       * 
       * @param application The MainApplication
       * @param timelineItem TimelineSummaryItem to be deleted
       * @return SUCCESS or a suitable error code
       */
      public ServiceStatus deleteTimelineActivity(MainApplication application,
              TimelineSummaryItem timelineItem, boolean isTimelineAll) {
          if (Settings.ENABLED_DATABASE_TRACE)
              trace(false, "DatabaseHelper.deleteTimelineActivity()");
  
          ServiceStatus status = ServiceStatus.SUCCESS;
  
          if (isTimelineAll) {
              status = ActivitiesTable.deleteTimelineActivities(mContext, timelineItem,
                      getWritableDatabase(), getReadableDatabase());
          } else {
              status = ActivitiesTable.deleteTimelineActivity(mContext, timelineItem,
                      getWritableDatabase(), getReadableDatabase());
          }
  
          if (status == ServiceStatus.SUCCESS) {
              // Update Notifications in the Notification Bar
              IPeopleService peopleService = application.getServiceInterface();
              long localContactId = 0L;
              if (timelineItem.mLocalContactId != null) {
                  localContactId = timelineItem.mLocalContactId;
              }
              peopleService.updateChatNotification(localContactId);
  
          }
          fireDatabaseChangedEvent(DatabaseChangeType.ACTIVITIES, true);
          return status;
      }
  
      /**
       * Add a list of new activities to the Activities table.
       * 
       * @param activityList contains the list of activity item
       * @return SUCCESS or a suitable error code
       * @see #deleteActivities(Integer)
       */
      public ServiceStatus addActivities(List<ActivityItem> activityList) {
          SQLiteDatabase writableDb = getWritableDatabase();
          ServiceStatus status = ActivitiesTable.addActivities(activityList, writableDb, mContext);
          ActivitiesTable.cleanupActivityTable(writableDb);
          fireDatabaseChangedEvent(DatabaseChangeType.ACTIVITIES, true);
          return status;
      }
  
      /***
       * Fetches a list of activity IDs from a given time.
       * 
       * @param activityIdList an empty list to be populated
       * @param timeStamp The oldest time that should be included in the list
       * @return SUCCESS or a suitable error code
       */
      public synchronized ServiceStatus fetchActivitiesIds(List<Long> activityIdList, Long timeStamp) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.fetchActivitiesIds() timeStamp[" + timeStamp + "]");
          }
          activityIdList.clear();
          ActivitiesTable.fetchActivitiesIds(activityIdList, timeStamp, getReadableDatabase());
          return ServiceStatus.SUCCESS;
      }
  
      /***
       * Fetches fires a database change event to the listeners.
       * 
       * @param type The type of database change (contacts, activity, etc)
       * @param isExternal true if this change came from the server, false if the
       *            change is from the client
       * @see #addEventCallback(Handler)
       * @see #removeEventCallback(Handler)
       * @see #fireSettingChangedEvent(PersistSettings)
       */
      public void fireDatabaseChangedEvent(DatabaseHelper.DatabaseChangeType type, boolean isExternal) {
          DbEventType event = new DbEventType();
          event.ordinal = type.ordinal();
          event.isExternal = isExternal;
          
          synchronized (mDbEvents) {
              if (mDbEvents.size() == 0) {
                  // Creating a DbEventTimerTask every time because of preemptive-ness
                  DbEventTimerTask dbEventTask = new DbEventTimerTask();                
                  mDbEventTimer.schedule(dbEventTask, DATABASE_EVENT_DELAY);
              }
              
              if (!mDbEvents.contains(event)) {
                  mDbEvents.add(event);
              }
          }
      }
  
      /***
       * Add a database change listener. The listener will be notified each time
       * the database is changed.
       * 
       * @param uiHandler The handler which will be notified
       * @see #fireDatabaseChangedEvent(DatabaseChangeType, boolean)
       * @see #fireSettingChangedEvent(PersistSettings)
       */
      public synchronized void addEventCallback(Handler uiHandler) {
          if (!mUiEventCallbackList.contains(uiHandler)) {
              mUiEventCallbackList.add(uiHandler);
          }
      }
  
      /***
       * Removes a database change listener. This must be called before UI
       * activities are destroyed.
       * 
       * @param uiHandler The handler which will be notified
       * @see #addEventCallback(Handler)
       */
      public synchronized void removeEventCallback(Handler uiHandler) {
          if (mUiEventCallbackList != null) {
              mUiEventCallbackList.remove(uiHandler);
          }
      }
  
      /***
       * Internal function to fire a setting changed event to listeners.
       * 
       * @param setting The setting that has changed with the new data
       * @see #addEventCallback(Handler)
       * @see #removeEventCallback(Handler)
       * @see #fireDatabaseChangedEvent(DatabaseChangeType, boolean)
       */
      private synchronized void fireSettingChangedEvent(PersistSettings setting) {
          fireEventToUi(ServiceUiRequest.SETTING_CHANGED_EVENT, 0, 0, setting);
      }
  
      /***
       * Internal function to send an event to all the listeners.
       * 
       * @param event The type of event
       * @param arg1 This value depends on the type of event
       * @param arg2 This value depends on the type of event
       * @param data This value depends on the type of event
       * @see #fireDatabaseChangedEvent(DatabaseChangeType, boolean)
       * @see #fireSettingChangedEvent(PersistSettings)
       */
      private void fireEventToUi(ServiceUiRequest event, int arg1, int arg2, Object data) {
          for (Handler handler : mUiEventCallbackList) {
              Message message = handler.obtainMessage(event.ordinal(), data);
              message.arg1 = arg1;
              message.arg2 = arg2;
              handler.sendMessage(message);
          }
      }
  
      /***
       * Function used by the contact sync engine to add a list of contacts to the
       * database.
       * 
       * @param contactList The list of contacts received from the server
       * @param syncToServer true if the contacts need to be sent to the server
       * @param syncToNative true if the contacts need to be added to the native
       *            phonebook
       * @return SUCCESS or a suitable error code
       * @see #addContact(Contact)
       */
      public ServiceStatus syncAddContactList(List<Contact> contactList, boolean syncToServer,
              boolean syncToNative) {
          if (Settings.ENABLED_DATABASE_TRACE)
              trace(false, "DatabaseHelper.syncAddContactList() syncToServer[" + syncToServer
                      + "] syncToNative[" + syncToNative + "]");
          if (!Settings.ENABLE_SERVER_CONTACT_SYNC) {
              syncToServer = false;
          }
          if (!Settings.ENABLE_UPDATE_NATIVE_CONTACTS) {
              syncToNative = false;
          }
  
          SQLiteDatabase writableDb = getWritableDatabase();
          
          boolean needFireDbUpdate = false;
  
          for (Contact contact : contactList) {
              contact.deleted = null;
              contact.localContactID = null;
              if (syncToNative) {
                  contact.nativeContactId = null;
              }
              if (syncToServer) {
                  contact.contactID = null;
                  contact.updated = null;
                  contact.synctophone = true;
              }
  
              try {
  
                  writableDb.beginTransaction();
                  ServiceStatus status = ContactsTable.addContact(contact, writableDb);
                  if (ServiceStatus.SUCCESS != status) {
                      LogUtils.logE("DatabaseHelper.syncAddContactList() Unable to add contact to contacts table, due to a database error");
                      return status;
                  }
  
                  List<ContactDetail.DetailKeys> orderList = new ArrayList<ContactDetail.DetailKeys>();
                  for (int i = 0; i < contact.details.size(); i++) {
                      final ContactDetail detail = contact.details.get(i);
                      
                      detail.localContactID = contact.localContactID;
                      detail.localDetailID = null;
                      if (syncToServer) {
                          detail.unique_id = null;
                      }
                      if (detail.order != null
                              && (detail.order.equals(ContactDetail.ORDER_PREFERRED))) {
                          if (orderList.contains(detail.key)) {
                              detail.order = ContactDetail.ORDER_NORMAL;
                          } else {
                              orderList.add(detail.key);
                          }
                      }
                      status = ContactDetailsTable.addContactDetail(detail, syncToServer,
                              (syncToNative && contact.synctophone), writableDb);
                      if (ServiceStatus.SUCCESS != status) {
                          LogUtils.logE("DatabaseHelper.syncAddContactList() Unable to add contact detail (for new contact), " +
                          			  "due to a database error. Contact ID[" + contact.localContactID + "]");
                          return status;
                      }
                  }
                  // AA: added the check to make sure that contacts with empty
                  // contact
                  // details are not stored
                  if (!contact.details.isEmpty()) {
                      status = ContactSummaryTable.addContact(contact, writableDb);
                      if (ServiceStatus.SUCCESS != status) {
  
                          return status;
                      }
                  }
  
                  if (contact.groupList != null) {
                      for (Long groupId : contact.groupList) {
                          if (groupId != -1
                                  && !ContactGroupsTable.addContactToGroup(contact.localContactID,
                                          groupId, writableDb)) {
                              return ServiceStatus.ERROR_DATABASE_CORRUPT;
                          }
                      }
                  }
  
                  if (contact.sources != null) {
                      for (String source : contact.sources) {
                          if (!ContactSourceTable.addContactSource(contact.localContactID, source,
                                  writableDb)) {
                              return ServiceStatus.ERROR_DATABASE_CORRUPT;
                          }
                      }
                  }
  
                  if (syncToServer) {
                      if (contact.groupList != null) {
                          for (Long groupId : contact.groupList) {
                              if (!ContactChangeLogTable.addGroupRel(contact.localContactID,
                                      contact.contactID, groupId, writableDb)) {
                                  return ServiceStatus.ERROR_DATABASE_CORRUPT;
                              }
                          }
                      }
                  }
  
                  /*
                   * FIXME: Hacking a check for me profile here using syncToNative and syncToServer
                   * The me contact does not use a static local contact id 
                   * which is ridiculous. Basically we have to check the syncToNative and syncToServer
                   * flags together with isMeProfile
                   * because luckily as of yet the they are only both false when its me profile 
                   * in case that's the contact being added.
                   */
                  String displayName = 
                      updateContactNameInSummary(writableDb, 
                                                 contact.localContactID, 
                                                 (!syncToNative && !syncToServer) || 
                                                 SyncMeDbUtils.isMeProfile(this, contact.localContactID));
                  if (null == displayName) {
                      return ServiceStatus.ERROR_DATABASE_CORRUPT;
                  }
                  // updating timeline
                  for (ContactDetail detail : contact.details) {
                  // we already have name, don't need to get it again
                      if (detail.key != ContactDetail.DetailKeys.VCARD_NAME) {
                          detail.localContactID = contact.localContactID;
                          detail.nativeContactId = contact.nativeContactId;
                          if (updateTimelineNames(detail, displayName, contact.contactID, writableDb)) {
                              needFireDbUpdate = true;
                          }
                      }
                  }
                  writableDb.setTransactionSuccessful();
              } finally {
                  writableDb.endTransaction();
              }
          }
          if (needFireDbUpdate) {
              fireDatabaseChangedEvent(DatabaseChangeType.ACTIVITIES, false);
          }
          return ServiceStatus.SUCCESS;
      }
  
      /***
       * Function used by the contact sync engine to modify a list of contacts in
       * the database.
       * 
       * @param contactList The list of contacts received from the server
       * @param syncToServer true if the contacts need to be sent to the server
       * @param syncToNative true if the contacts need to be modified in the
       *            native phonebook
       * @return SUCCESS or a suitable error code
       */
      public ServiceStatus syncModifyContactList(List<Contact> contactList, boolean syncToServer,
              boolean syncToNative) {
      	
          if (Settings.ENABLED_DATABASE_TRACE)
              trace(false, "DatabaseHelper.syncModifyContactList() syncToServer[" + syncToServer
                      + "] syncToNative[" + syncToNative + "]");
          if (!Settings.ENABLE_SERVER_CONTACT_SYNC) {
              syncToServer = false;
          }
          if (!Settings.ENABLE_UPDATE_NATIVE_CONTACTS) {
              syncToNative = false;
          }
          SQLiteDatabase writableDb = getWritableDatabase();
          boolean needFireDbUpdate = false;
  
          for (Contact contact : contactList) {
              if (syncToServer) {
                  contact.updated = null;
              }
  
              try {
                  writableDb.beginTransaction();
                  ServiceStatus status = ContactsTable.modifyContact(contact, writableDb);
                  if (ServiceStatus.SUCCESS != status) {
                      LogUtils.logE("DatabaseHelper.syncModifyContactList() Unable to modify contact, due to a database error");
                      return status;
                  }
  
                  status = ContactSummaryTable.modifyContact(contact, writableDb);
                  if (ServiceStatus.SUCCESS != status) {
                      return ServiceStatus.ERROR_DATABASE_CORRUPT;
                  }
  
                  if (contact.groupList != null) {
                      status = ContactGroupsTable.modifyContact(contact, writableDb);
                      if (ServiceStatus.SUCCESS != status) {
                          return status;
                      }
                  }
  
                  if (contact.sources != null) {
                      if (!ContactSourceTable.deleteAllContactSources(contact.localContactID, writableDb)) {
                          return ServiceStatus.ERROR_DATABASE_CORRUPT;
                      }
                      for (String source : contact.sources) {
                          if (!ContactSourceTable.addContactSource(contact.localContactID, source,
                                  writableDb)) {
                              return ServiceStatus.ERROR_DATABASE_CORRUPT;
                          }
                      }
                  }
  
                  // END updating timeline events
  
                  // Update the summary with the new contact
                  String displayName = updateContactNameInSummary(writableDb, 
                          contact.localContactID, 
                          SyncMeDbUtils.isMeProfile(this, contact.localContactID));
                  if (null == displayName) {
                      return ServiceStatus.ERROR_DATABASE_CORRUPT;
                  }
                  // updating phone no
                  for (ContactDetail detail : contact.details) {
                      detail.localContactID = contact.localContactID;
                      detail.nativeContactId = contact.nativeContactId;
                      if (updateTimelineNames(detail, displayName, contact.contactID, writableDb)) {
                          needFireDbUpdate = true;
                      }
                  }
                  writableDb.setTransactionSuccessful();
              } finally {
                  writableDb.endTransaction();
              }
          }
          
          if (needFireDbUpdate) {
              fireDatabaseChangedEvent(DatabaseChangeType.ACTIVITIES, false);
          }
          return ServiceStatus.SUCCESS;
      }
  
      /***
       * Function used by the contact sync engine to delete a list of contacts
       * from the database.
       * 
       * @param contactIdList The list of contact IDs received from the server (at
       *            least localId should be set)
       * @param syncToServer true if the contacts need to be deleted from the
       *            server
       * @param syncToNative true if the contacts need to be deleted from the
       *            native phonebook
       * @return SUCCESS or a suitable error code
       * @see #deleteContact(long)
       */
      public ServiceStatus syncDeleteContactList(List<ContactsTable.ContactIdInfo> contactIdList,
              boolean syncToServer, boolean syncToNative) {
          if (Settings.ENABLED_DATABASE_TRACE)
              trace(false, "DatabaseHelper.syncDeleteContactList() syncToServer[" + syncToServer
                      + "] syncToNative[" + syncToNative + "]");
          if (!Settings.ENABLE_SERVER_CONTACT_SYNC) {
              syncToServer = false;
          }
          if (!Settings.ENABLE_UPDATE_NATIVE_CONTACTS) {
              syncToNative = false;
          }
  
          SQLiteDatabase writableDb = getWritableDatabase();
  
          for (ContactsTable.ContactIdInfo contactIdInfo : contactIdList) {
  
              try {
                  writableDb.beginTransaction();
                  if (syncToNative && contactIdInfo.mergedLocalId == null) {
                      if (!NativeChangeLogTable.addDeletedContactChange(contactIdInfo.localId,
                              contactIdInfo.nativeId, writableDb)) {
                          return ServiceStatus.ERROR_DATABASE_CORRUPT;
                      }
                  }
  
                  if (syncToServer) {
                      if (!ContactChangeLogTable.addDeletedContactChange(contactIdInfo.localId,
                              contactIdInfo.serverId, syncToServer, writableDb)) {
                          return ServiceStatus.ERROR_DATABASE_CORRUPT;
                      }
                  }
                  if (!ContactGroupsTable.deleteContact(contactIdInfo.localId, writableDb)) {
                      return ServiceStatus.ERROR_DATABASE_CORRUPT;
                  }
                  if (SyncMeDbUtils.getMeProfileLocalContactId(this) != null
                          && SyncMeDbUtils.getMeProfileLocalContactId(this).longValue() == contactIdInfo.localId) {
                      ServiceStatus status = StateTable.modifyMeProfileID(null, writableDb);
                      if (ServiceStatus.SUCCESS != status) {
                          return status;
                      }
                      SyncMeDbUtils.setMeProfileId(null);
                      PresenceDbUtils.resetMeProfileIds();
                  }
                  ServiceStatus status = ContactSummaryTable.deleteContact(contactIdInfo.localId, writableDb);
                  if (ServiceStatus.SUCCESS != status) {
                      return status;
                  }
                  status = ContactDetailsTable.deleteDetailByContactId(contactIdInfo.localId, writableDb);
                  if (ServiceStatus.SUCCESS != status && ServiceStatus.ERROR_NOT_FOUND != status) {
                      return status;
                  }
                  status = ContactsTable.deleteContact(contactIdInfo.localId, writableDb);
                  if (ServiceStatus.SUCCESS != status) {
                      return status;
                  }
  
                  if (!deleteThumbnail(contactIdInfo.localId))
                      LogUtils.logE("Not able to delete thumbnail for: " + contactIdInfo.localId);
  
                  // timeline
                  ActivitiesTable.removeTimelineContactData(contactIdInfo.localId, writableDb);
  
                  writableDb.setTransactionSuccessful();
              } finally {
                  writableDb.endTransaction();
              }
          }
  
          return ServiceStatus.SUCCESS;
      }
      
      
      /***
       * Function used by the contact sync engine to merge contacts which are
       * marked as duplicate by the server. This involves moving native
       * information from one contact to the other, before deleting it.
       * 
       * @param contactIdList The list of contact IDs (localId, serverId and
       *            mergedLocalId should be set)
       * @return SUCCESS or a suitable error code
       */
      public ServiceStatus syncMergeContactList(List<ContactsTable.ContactIdInfo> contactIdList) {
          if (Settings.ENABLED_DATABASE_TRACE)
              trace(false, "DatabaseHelper.syncMergeContactList()");
          
          List<ContactDetail> detailInfoList = new ArrayList<ContactDetail>();
          SQLiteDatabase writableDb = getWritableDatabase();
          SQLiteStatement contactStatement = null, contactSummaryStatement = null;
          
          try {
              contactStatement = ContactsTable.mergeContactStatement(writableDb);
              contactSummaryStatement = ContactSummaryTable.mergeContactStatement(writableDb);
              
              writableDb.beginTransaction();
              for (int i = 0; i < contactIdList.size(); i++) {
                  ContactsTable.ContactIdInfo contactIdInfo = contactIdList.get(i);
                  if (contactIdInfo.mergedLocalId != null) {
                      contactIdInfo.nativeId = ContactsTable.fetchNativeFromLocalId(writableDb, contactIdInfo.localId);
                      
                      LogUtils.logI("DatabaseHelper.syncMergeContactList - Copying native Ids from duplicate to original contact: Dup ID "
                                      + contactIdInfo.localId
                                      + ", Org ID "
                                      + contactIdInfo.mergedLocalId
                                      + ", Nat ID "
                                      + contactIdInfo.nativeId);
      
                      ServiceStatus status = ContactsTable.mergeContact(contactIdInfo, contactStatement);
                      if(status != ServiceStatus.SUCCESS) {
                          return status;
                      }
                      
                      status = ContactSummaryTable.mergeContact(contactIdInfo, contactSummaryStatement);
                      if(status != ServiceStatus.SUCCESS) {
                          return status;
                      }
  
                      status = ContactDetailsTable.fetchNativeInfo(contactIdInfo.localId, detailInfoList, writableDb);
                      if(status != ServiceStatus.SUCCESS) {
                          return status;
                      }
                      
                      status = ContactDetailsTable.mergeContactDetails(contactIdInfo, detailInfoList, writableDb);
                      if(status != ServiceStatus.SUCCESS) {
                          return status;
                      }
                  }
              }
              writableDb.setTransactionSuccessful();
          } 
          finally {
              writableDb.endTransaction();
              if(contactStatement != null) {
                  contactStatement.close();
                  contactStatement = null;
              }
              
              if(contactSummaryStatement != null) {
                  contactSummaryStatement.close();
                  contactSummaryStatement = null;
              }
          }
        
          LogUtils.logI("DatabaseHelper.syncMergeContactList - Deleting duplicate contacts");
          return syncDeleteContactList(contactIdList, false, true);
      }
      
  
      /***
       * Function used by the contact sync engine to add a list of contact details
       * to the database.
       * 
       * @param detailList The list of details received from the server
       * @param syncToServer true if the details need to be sent to the server
       * @param syncToNative true if the contacts need to be added to the native
       *            phonebook
       * @return SUCCESS or a suitable error code
       * @see #addContactDetail(ContactDetail)
       */
      public ServiceStatus syncAddContactDetailList(List<ContactDetail> detailList,
              boolean syncToServer, boolean syncToNative) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              trace(false, "DatabaseHelper.syncAddContactDetailList() syncToServer[" + syncToServer
                      + "] syncToNative[" + syncToNative + "]");
          }
          if (!Settings.ENABLE_SERVER_CONTACT_SYNC) {
              syncToServer = false;
          }
          if (!Settings.ENABLE_UPDATE_NATIVE_CONTACTS) {
              syncToNative = false;
          }
  
          boolean needFireDbUpdate = false; 
          SQLiteDatabase db = getWritableDatabase();
  
          for (ContactDetail contactDetail : detailList) {
              
              contactDetail.localDetailID = null;
              if (syncToServer) {
                  contactDetail.unique_id = null;
              }
              if (syncToNative) {
                  contactDetail.nativeDetailId = null;
              }
              if (contactDetail.localContactID == null) {
                  return ServiceStatus.ERROR_NOT_FOUND;
              }
              try {
                  db.beginTransaction();
                  ContactsTable.ContactIdInfo contactIdInfo = ContactsTable.validateContactId(
                          contactDetail.localContactID, db);
                  if (contactIdInfo == null) {
                      return ServiceStatus.ERROR_NOT_FOUND;
                  }
                  contactDetail.serverContactId = contactIdInfo.serverId;
                  if (contactIdInfo.syncToPhone) {
                      contactDetail.syncNativeContactId = contactIdInfo.nativeId;
                  } else {
                      contactDetail.syncNativeContactId = -1;
                  }
                  if (contactDetail.order != null
                          && contactDetail.order.equals(ContactDetail.ORDER_PREFERRED)) {
                      ContactDetailsTable.removePreferred(contactDetail.localContactID,
                              contactDetail.key, db);
                  }
                  ServiceStatus status = ContactDetailsTable.addContactDetail(contactDetail,
                          syncToServer, syncToNative, db);
                  if (ServiceStatus.SUCCESS != status) {
                      return status;
                  }
  
                  // Whenever the photo URL is updated, the photoloaded flag in
                  // ContactSummaryTable should be reset to 0 so that when the
                  // thumbnails are downloaded later on, the new thumbnail shall
                  // also be downloaded.
                  // When the picture is being from the client we don't need to set the flag to "TRUE",
                  // in order not to override the new picture before it is uploaded.
                  if (contactDetail.key == ContactDetail.DetailKeys.PHOTO && TextUtils.isEmpty(contactDetail.photo_url)) {
                          ContactSummaryTable.modifyPictureLoadedFlag(contactDetail.localContactID,
                                  false, db);
                  }
                  String displayName = updateContactNameInSummary(db,
                          contactDetail.localContactID, 
                          SyncMeDbUtils.isMeProfile(this, contactDetail.localContactID));
                  if (null == displayName) {
                      return ServiceStatus.ERROR_DATABASE_CORRUPT;
                  }
  
                  if (updateTimelineNames(contactDetail, displayName, null, db)) {
                      needFireDbUpdate = true;
                  }
  
                  db.setTransactionSuccessful();
              } finally {
                  db.endTransaction();
              }
          }
          
          if (needFireDbUpdate) {
              fireDatabaseChangedEvent(DatabaseChangeType.ACTIVITIES, false);
          }
  
          return ServiceStatus.SUCCESS;
      }
  
  
      /**
       * Updates the contents of the activities table when a contact detail
       * changes.
       * 
       * @param cd The new or modified contact detail
       * @param contactFriendlyName Name of contact (if known)
       * @param serverId if known
       * @param db Writable SQLite database for the update
       * @return true if the Activities table was updated, false otherwise
       */
      private boolean updateTimelineNames(ContactDetail cd, String contactFriendlyName, Long serverId,
              SQLiteDatabase db) {
          if (cd.key == ContactDetail.DetailKeys.VCARD_NAME) {
              if (contactFriendlyName != null) {
                  ActivitiesTable.updateTimelineContactNameAndId(contactFriendlyName,
                          cd.localContactID, db);
                  return true;    
              }
          }
          if (cd.key == ContactDetail.DetailKeys.VCARD_PHONE) {
              if (contactFriendlyName == null) {
                  ContactSummary cs = new ContactSummary();
                  if (ContactSummaryTable.fetchSummaryItem(cd.localContactID, cs, db) == ServiceStatus.SUCCESS) {
                      contactFriendlyName = cs.formattedName;
                  }
              }
              if (contactFriendlyName != null) {
                  Long cId = serverId;
                  if (cId == null) {
                      cId = ContactsTable.fetchServerId(cd.localContactID, db);
                  }
                  ActivitiesTable.updateTimelineContactNameAndId(cd.getTel(), contactFriendlyName,
                          cd.localContactID, cId, db);
                  return true;
              } else {
                  LogUtils.logE("updateTimelineNames() failed to fetch summary Item");
              }
          }
          return false;
      }
  
      /***
       * Function used by the contact sync engine to modify a list of contact
       * details in the database.
       * 
       * @param contactDetailList The list of details received from the server
       * @param serverIdList A list of server IDs if known, or null
       * @param syncToServer true if the details need to be sent to the server
       * @param syncToNative true if the contacts need to be added to the native
       *            phonebook
       * @return SUCCESS or a suitable error code
       * @see #modifyContactDetail(ContactDetail)
       */
      public ServiceStatus syncModifyContactDetailList(List<ContactDetail> contactDetailList,
              boolean syncToServer, boolean syncToNative) {
          if (Settings.ENABLED_DATABASE_TRACE)
              trace(false, "DatabaseHelper.syncModifyContactDetailList() syncToServer["
                      + syncToServer + "] syncToNative[" + syncToNative + "]");
          if (!Settings.ENABLE_SERVER_CONTACT_SYNC) {
              syncToServer = false;
          }
          if (!Settings.ENABLE_UPDATE_NATIVE_CONTACTS) {
              syncToNative = false;
          }
  
          boolean needFireDbUpdate = false;
          SQLiteDatabase db = getWritableDatabase();
  
          for (ContactDetail contactDetail : contactDetailList) {
              ContactsTable.ContactIdInfo contactIdInfo = ContactsTable.validateContactId(
                      contactDetail.localContactID, db);
              if (contactIdInfo == null) {
                  return ServiceStatus.ERROR_NOT_FOUND;
              }
              contactDetail.serverContactId = contactIdInfo.serverId;
              if (contactIdInfo.syncToPhone) {
                  contactDetail.syncNativeContactId = contactIdInfo.nativeId;
              } else {
                  contactDetail.syncNativeContactId = -1;
              }
              try {
  
                  db.beginTransaction();
                  if (contactDetail.order != null
                          && contactDetail.order.equals(ContactDetail.ORDER_PREFERRED)) {
                      ContactDetailsTable.removePreferred(contactDetail.localContactID,
                              contactDetail.key, db);
                  }
                  ServiceStatus status = ContactDetailsTable.modifyDetail(contactDetail,
                          syncToServer, syncToNative, db);
                  if (ServiceStatus.SUCCESS != status) {
                      return status;
                  }
  
                  // Whenever the photo URL is updated, the photoloaded flag in
                  // ContactSummaryTable should be reset to 0 so that when the
                  // thumbnails are downloaded later on, the new thumbnail shall
                  // also be downloaded.
                  // When the picture is being from the client we don't need to set the flag to "TRUE",
                  // in order not to override the new picture before it is uploaded.
                  if (ContactDetail.DetailKeys.PHOTO == contactDetail.key && TextUtils.isEmpty(contactDetail.photo_url)) {
                      ContactSummaryTable.modifyPictureLoadedFlag(contactDetail.localContactID,
                              false, db);
                  }
  
                  String displayName = updateContactNameInSummary(db,
                          contactDetail.localContactID, 
                          SyncMeDbUtils.isMeProfile(this, contactDetail.localContactID));
                  if (null == displayName) {
                      return ServiceStatus.ERROR_DATABASE_CORRUPT;
                  }
                  if (updateTimelineNames(contactDetail, displayName, null, db)) {
                      needFireDbUpdate = true;
                  }
                  db.setTransactionSuccessful();
              } finally {
                  db.endTransaction();
              }
          }
          
          if (needFireDbUpdate) {
              fireDatabaseChangedEvent(DatabaseChangeType.ACTIVITIES, false);
          }
  
          return ServiceStatus.SUCCESS;
      }
  
      /***
       * Function used by the contact sync engine to delete a list of contact
       * details from the database.
       * 
       * @param contactDetailList The list of details which has been deleted on
       *            the server
       * @param serverIdList A list of server IDs if known, or null
       * @param syncToServer true if the details need to be sent to the server
       * @param syncToNative true if the contacts need to be added to the native
       *            phonebook
       * @param meProfile - TRUE if the added contact is Me profile.  
       * @return SUCCESS or a suitable error code
       * @see #deleteContactDetail(long)
       */
      public ServiceStatus syncDeleteContactDetailList(List<ContactDetail> contactDetailList,
              boolean syncToServer, boolean syncToNative) {
          if (Settings.ENABLED_DATABASE_TRACE)
              trace(false, "DatabaseHelper.syncDeleteContactDetailList() syncToServer["
                      + syncToServer + "] syncToNative[" + syncToNative + "]");
          if (!Settings.ENABLE_SERVER_CONTACT_SYNC) {
              syncToServer = false;
          }
          if (!Settings.ENABLE_UPDATE_NATIVE_CONTACTS) {
              syncToNative = false;
          }
  
          SQLiteDatabase db = getWritableDatabase();
          boolean needFireDbUpdate = false;
          
          for (ContactDetail contactDetail : contactDetailList) {
              if ((contactDetail.serverContactId == null) || (contactDetail.serverContactId == -1)) {
                  ContactsTable.ContactIdInfo contactIdInfo = ContactsTable.validateContactId(
                          contactDetail.localContactID, db);
                  if (contactIdInfo == null) {
                      return ServiceStatus.ERROR_NOT_FOUND;
                  }
                  contactDetail.nativeContactId = contactIdInfo.nativeId;
                  contactDetail.serverContactId = contactIdInfo.serverId;
              }
  
              try {
                  db.beginTransaction();
                  if (syncToNative) {
                      if (!NativeChangeLogTable.addDeletedContactDetailChange(contactDetail, db)) {
                          return ServiceStatus.ERROR_DATABASE_CORRUPT;
                      }
                  }
                  if (syncToServer) {
                      if (!ContactChangeLogTable.addDeletedContactDetailChange(contactDetail,
                              syncToServer, db)) {
                          return ServiceStatus.ERROR_DATABASE_CORRUPT;
                      }
                  }
                  if (!ContactDetailsTable.deleteDetailByDetailId(contactDetail.localDetailID, db)) {
                      return ServiceStatus.ERROR_DATABASE_CORRUPT;
                  }
  
                  // Whenever the photo URL is updated, the photoloaded flag in
                  // ContactSummaryTable should be reset to 0 so that when the
                  // thumbnails are downloaded later on, the new thumbnail shall
                  // also be downloaded.
                  // When the picture is being from the client we don't need to set the flag to "TRUE",
                  // in order not to override the new picture before it is uploaded.
                  if (contactDetail.key == ContactDetail.DetailKeys.PHOTO && TextUtils.isEmpty(contactDetail.photo_url)) {
                      ContactSummaryTable.modifyPictureLoadedFlag(contactDetail.localContactID,
                              false, db);
                      deleteThumbnail(contactDetail.localContactID);
                  }
  
                  String displayName = updateContactNameInSummary(db,
                          contactDetail.localContactID,
                          SyncMeDbUtils.isMeProfile(this, contactDetail.localContactID));
                  if (displayName == null) {
                      return ServiceStatus.ERROR_DATABASE_CORRUPT;
                  }
                  
                  if (updateTimelineNames(contactDetail, displayName, contactDetail.localContactID, db)) {
                      needFireDbUpdate = true;
                  }
                  db.setTransactionSuccessful();
              } finally {
                  db.endTransaction();
              }
          }
  
          if (needFireDbUpdate) {
              fireDatabaseChangedEvent(DatabaseChangeType.ACTIVITIES, false);
          }
          return ServiceStatus.SUCCESS;
      }
  
      /***
       * Fetches the outer contact object information (no details, groups or
       * sources are included).
       * 
       * @param localContactId The local ID of the contact to fetch
       * @param baseContact An empty Contact object which will be filled with the
       *            data
       * @return SUCCESS or a suitable error code
       * @see #fetchContact(long, Contact)
       */
      private ServiceStatus fetchBaseContact(long localContactId, Contact baseContact,
              SQLiteDatabase db) {
          ServiceStatus status = ContactsTable.fetchContact(localContactId, baseContact, db);
          if (ServiceStatus.SUCCESS != status) {
              return status;
          }
          if (baseContact.groupList == null) {
              baseContact.groupList = new ArrayList<Long>();
          }
          if (!ContactGroupsTable.fetchContactGroups(localContactId, baseContact.groupList, db)) {
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          }
          if (baseContact.sources == null) {
              baseContact.sources = new ArrayList<String>();
          }
          if (!ContactSourceTable.fetchContactSources(localContactId, baseContact.sources, db)) {
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          }
          return ServiceStatus.SUCCESS;
      }
  
      /***
       * Fetches the server Id of a contact.
       * 
       * @param localContactId The local ID of the contact
       * @return The server Id of the contact, or null the contact has not yet
       *         been synchronised
       * @see #fetchContactByServerId(Long, Contact)
       * @see #modifyContactServerId(long, Long, Long)
       */
      public Long fetchServerId(long localContactId) {
          trace(false, "DatabaseHelper.fetchServerId() localContactId[" + localContactId + "]");
          ContactsTable.ContactIdInfo info = ContactsTable.validateContactId(localContactId,
                  getReadableDatabase());
          if (info == null) {
              return null;
          }
          return info.serverId;
      }
  
      /***
       * Remove all user data (Thumbnails, Database, Flags) from the device and
       * notifies the engine manager.
       */
      public void removeUserData() {
          trace(false, "DatabaseHelper.removeUserData()");
  
          String thumbnailPath = ThumbnailUtils.thumbnailPath(null);
          deleteDirectory(new File(thumbnailPath));
          deleteDatabase();
  
          SyncMeDbUtils.setMeProfileId(null);
  
          PresenceDbUtils.resetMeProfileIds();
  
      }
  
      /***
       * Deletes a given Thumbnail
       * 
       * @param localContactID The local Id of the contact with the Thumbnail
       */
      private boolean deleteThumbnail(Long localContactID) {
          trace(false, "DatabaseHelper.deleteThumbnail() localContactID[" + localContactID + "]");
          String thumbnailPath = ThumbnailUtils.thumbnailPath(localContactID);
          if (thumbnailPath != null) {
              File file = new File(thumbnailPath);
              if (file.exists()) {
                  return file.delete();
              }
          }
          // if the file was not there the deletion was also correct
          return true;
      }
  
      /***
       * Fetches a contact, given a server Id.
       * 
       * @param contactServerId The server ID of the contact to fetch
       * @param contact An empty Contact object which will be filled with the data
       * @return SUCCESS or a suitable error code
       * @see #modifyContactServerId(long, Long, Long)
       * @see #fetchServerId(long)
       */
      public ServiceStatus fetchContactByServerId(Long contactServerId, Contact contact) {
          final SQLiteStatement statement = ContactsTable
                  .fetchLocalFromServerIdStatement(getReadableDatabase());
          Long localId = ContactsTable.fetchLocalFromServerId(contactServerId, statement);
          if (localId == null) {
              return ServiceStatus.ERROR_NOT_FOUND;
          }
          
          if(statement != null) {
              statement.close();
          }
          return fetchContact(localId, contact);
      }
  
      /***
       * Utility function which compares two contact details to determine if they
       * refer to the same detail (the values may be different). TODO: Move to
       * utility class
       * 
       * @param d1 The first contact detail to compare
       * @param d2 The second contact detail to compare
       * @return true if they are the same
       * @see #hasDetailChanged(ContactDetail, ContactDetail)
       */
      public static boolean doDetailsMatch(ContactDetail d1, ContactDetail d2) {
          if (d1.key == null || !d1.key.equals(d2.key)) {
              return false;
          }
          if (d1.unique_id == null && d2.unique_id == null) {
              return true;
          }
          if (d1.unique_id != null && d1.unique_id.equals(d2.unique_id)) {
              return true;
          }
          return false;
      }
  
      /***
       * Utility function which compares two contact details to determine if they
       * have the same value. TODO: Move to utility class
       * 
       * @param oldDetail The first contact detail to compare
       * @param newDetail The second contact detail to compare
       * @return true if they have the same value
       * @see #doDetailsMatch(ContactDetail, ContactDetail)
       */
      public static boolean hasDetailChanged(ContactDetail oldDetail, ContactDetail newDetail) {
          if (newDetail.value != null && !newDetail.value.equals(oldDetail.value)) {
              return true;
          }
          if (newDetail.alt != null && !newDetail.alt.equals(oldDetail.alt)) {
              return true;
          }
          if (newDetail.keyType != null && !newDetail.keyType.equals(oldDetail.keyType)) {
              return true;
          }
          if (newDetail.location != null && !newDetail.location.equals(oldDetail.location)) {
              return true;
          }
          if (newDetail.order != null && !newDetail.order.equals(oldDetail.order)) {
              return true;
          }
          if (newDetail.photo != null && !newDetail.photo.equals(oldDetail.photo)) {
              return true;
          }
          if (newDetail.photo_mime_type != null
                  && !newDetail.photo_mime_type.equals(oldDetail.photo_mime_type)) {
              return true;
          }
          if (newDetail.photo_url != null && !newDetail.photo_url.equals(oldDetail.photo_url)) {
              return true;
          }
          return false;
  
      }
  
      /***
       * Add timeline events to the database.
       * 
       * @param syncItemList The list of items to be added
       * @param isCallLog true if the list has come from the call-log, false
       *            otherwise
       * @return SUCCESS or a suitable error code
       * @see #deleteActivities(Integer)
       * @see #fetchActivitiesIds(List, Long)
       */
      public ServiceStatus addTimelineEvents(ArrayList<TimelineSummaryItem> syncItemList,
              boolean isCallLog) {
          if (Settings.ENABLED_DATABASE_TRACE)
              trace(false, "DatabaseHelper.addTimelineEvents() isCallLog[" + isCallLog + "]");
          SQLiteDatabase writableDb = getWritableDatabase();
          ServiceStatus status = ActivitiesTable.addTimelineEvents(syncItemList, isCallLog,
                  writableDb);
          
          if (ServiceStatus.SUCCESS == status) {
              fireDatabaseChangedEvent(DatabaseChangeType.ACTIVITIES, true);
          }
          return status;
      }
  
      /***
       * Utility function to create a where clause string from a list of
       * conditions. TODO: Move to utility class
       * 
       * @param field The name of the table field to be compared
       * @param itemList The list of items to be compared against the field
       * @param clause This can be "AND", "OR" or any other SQL clause
       * @return The WHERE clause string (without the WHERE)
       */
      public static String createWhereClauseFromList(String field, Object[] itemList, String clause) {
          if (itemList == null || itemList.length == 0) {
              return "";
          }
          StringBuffer whereClause = new StringBuffer();
          whereClause.append("(");
          final boolean isEnum = (itemList[0].getClass().getEnumConstants() != null);
          for (int i = 0; i < itemList.length; i++) {
              Object item = itemList[i];
              if (isEnum) {
                  item = ((Enum<?>)itemList[i]).ordinal();
              }
              whereClause.append(field + "=" + item.toString());
              if (i < itemList.length - 1) {
                  whereClause.append(" " + clause + " ");
              }
          }
          whereClause.append(")");
          return whereClause.toString();
      }
  
      /**
       * Determines if the me profile avatar needs to be uploaded onto the server.
       * 
       * @return true if the avatar has changed and needs to be uploaded
       * @see #modifyMeProfileAvatarChangedFlag(boolean)
       */
      public boolean isMeProfileAvatarChanged() {
          return mMeProfileAvatarChangedFlag;
      }
  
      /***
       * Logs Database activity when the Settings.ENABLED_DATABASE_TRACE flag is
       * set to true.
       * 
       * @param write true if this is debug trace, false otherwise
       * @param input String to Log at Info level
       */
      public static void trace(boolean write, String input) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              if (write) {
                  Log.i(LOG_TAG, input);
              } else {
                  Log.d(LOG_TAG, input);
              }
          }
      }
  
      /***
       * Copies a snapshot of the database to the SD Card - Used for testing only.
       * 
       * @return A string which contains a description of the result
       */
      public String copyDatabaseToSd(String info) {
          String fileName = "/sdcard/people_" + info + "_" + System.currentTimeMillis() + ".db";
          close();
  
          InputStream in = null;
          OutputStream out = null;
  
          try {
              File sourceFile = mContext.getDatabasePath(DATABASE_NAME);
              File targetFile = new File(fileName);
              in = new FileInputStream(sourceFile);
              out = new FileOutputStream(targetFile);
              final int size = 1024;
              byte[] buf = new byte[size];
              int len;
              while ((len = in.read(buf)) > 0) {
                  out.write(buf, 0, len);
              }
              in.close();
              out.close();
              return "DatabaseHelper.copyDatabaseToSd() Database copied to SD Card as [" + fileName
                      + "]";
          } catch (FileNotFoundException ex) {
              return "DatabaseHelper.copyDatabaseToSd() File not found [" + ex.getMessage()
                      + "]' in the specified directory.";
          } catch (IOException e) {
              return "DatabaseHelper.copyDatabaseToSd() IOException[" + e.getMessage() + "]";
          } finally {
              CloseUtils.close(in);
              CloseUtils.close(out);
          }
      }
  
      /**
       * Deletes a directory and all its contents including sub-directories.
       * 
       * @param path file location
       * @return true if directory deleted otherwise false
       */
      private static boolean deleteDirectory(final File path) {
          boolean isDeletionSuccess = true;
          
          if (path.exists()) {
              File[] files = path.listFiles();
              for (int i = 0; i < files.length; i++) {
                  if (files[i].isDirectory()) {
                      if (!deleteDirectory(files[i])) {
                          isDeletionSuccess = false;
                      }
                  } else {
                      if (!files[i].delete()) {
                          isDeletionSuccess = false;
                      }
  
                  }
              }
          }
          if (isDeletionSuccess) {
              return (path.delete());
          } else {
              return isDeletionSuccess;
          }
      }
  
      /**
       * find the native contact in the database.
       * 
       * @param c contact
       * @return contact details of the particular contact
       */
      public boolean findNativeContact(Contact c) {
          return ContactDetailsTable.findNativeContact(c, getWritableDatabase());
      }
  
      /***
       * Stores a flag in the database indicating that the me profile avatar has
       * changed. The avatar will be uploaded to the server shortly.
       */
      public void markMeProfileAvatarChanged() {
          modifyMeProfileAvatarChangedFlag(true);
          // fireDatabaseChangedEvent(DatabaseChangeType.ME_PROFILE, false);
      }
  
      /**
       * Updates the ContactSummary table with the new/changed Contact.
       * @param writableDatabase - SQLiteDatabase writable database.
       * @param localContactId - long contact local id.
       * @param isMeProfile - boolean that indicates if the localContactId belongs to Me Profile.
  	 * @return The updated name, may be null in failure situations
       */
      public String updateContactNameInSummary(SQLiteDatabase writableDatabase,
              long localContactId, boolean isMeProfile) {
  
          Contact contact = new Contact();
          ServiceStatus status = fetchBaseContact(localContactId, contact, writableDatabase);
          if (ServiceStatus.SUCCESS != status) {
              return null;
          }
          status = ContactDetailsTable.fetchContactDetails(localContactId, contact.details,
                  writableDatabase);
          if (ServiceStatus.SUCCESS != status) {
              return null;
          }
          return ContactSummaryTable.updateContactDisplayName(contact, writableDatabase, isMeProfile);
      }
  
      public List<Contact> fetchContactList() {
          return ContactsTable.fetchContactList(getReadableDatabase());
      }
  
      /**
       * Adds a native contact to the people database and makes sure that the
       * related tables are updated (Contact, ContactDetail, ContactSummary and
       * Activities).
       * 
       * @param contact the contact to add
       * @return true if successful, false otherwise
       */
      public boolean addNativeContact(ContactChange[] contact) {
  
          if (contact == null || contact.length <= 0)
              return false;
  
          final SQLiteDatabase wdb = getWritableDatabase();
  
          try {
  
              wdb.beginTransaction();
  
              // add the contact in the Contacts table
              final ContentValues values = ContactsTable.getNativeContentValues(contact[0]);
              final long internalContactId = ContactsTable.addContact(values, wdb);
  
              if (internalContactId != -1) {
  
                  // sets the newly created internal contact id to all the
                  // ContactChange
                  setInternalContactId(contact, internalContactId);
  
                  // the contact was created in the contacts table, now add the
                  // details
                  if (!ContactDetailsTable.addNativeContactDetails(contact, wdb)) {
  
                      return false;
                  }
  
                  // from this point, legacy code will be called...
                  final Contact legacyContact = convertNativeContactChanges(contact);
  
                  // ...update timeline and contact summary with legacy code...
                  if (!updateTimelineAndContactSummaryWithLegacyCode(legacyContact, wdb)) {
  
                      return false;
                  }
  
              } else {
  
                  return false;
              }
  
              wdb.setTransactionSuccessful();
  
              return true;
          } catch (Exception e) {
  
              LogUtils.logE("addNativeContact() - Error:" + e);
          } finally {
  
              if (wdb != null) {
                  wdb.endTransaction();
              }
          }
  
          return false;
      }
  
      /**
       * Updates the Timeline and ContactSummary tables with a new contact. Note:
       * this method assumes that it being called within a transaction
       * 
       * @param contact the contact to take info from
       * @param writableDb the db to use to write the updates
       * @return true if successful, false otherwise
       */
      private boolean updateTimelineAndContactSummaryWithLegacyCode(Contact contact,
              SQLiteDatabase writableDb) {
  
          if (!contact.details.isEmpty()) {
              final ServiceStatus status = ContactSummaryTable.addContact(contact, writableDb);
              if (ServiceStatus.SUCCESS != status) {
                  return false;
              }
          }
          // update the summary with the new contact, pass "false" as Me Profile can't be a native contact
          String displayName = updateContactNameInSummary(writableDb, contact.localContactID, false);
          if (displayName == null) {
              return false;
          }
          for (int i = 0; i < contact.details.size(); i++) {
               final ContactDetail detail = contact.details.get(i);
              // updating timeline
               if (detail.key != ContactDetail.DetailKeys.VCARD_NAME) {
                   detail.localContactID = contact.localContactID;
                   detail.nativeContactId = contact.nativeContactId;
                   updateTimelineNames(detail, displayName, contact.contactID, writableDb);
               }
          }
          return true;
      }
  
      /**
       * Sets the internalContactId for all the ContactChange provided.
       * 
       * @param contact the array of ContactChange to update
       * @param internalContactId the id to set
       */
      private void setInternalContactId(ContactChange[] contact, long internalContactId) {
  
          for (int i = 0; i < contact.length; i++) {
  
              contact[i].setInternalContactId(internalContactId);
          }
      }
  
      /**
       * Converts an array of ContactChange into a Contact object.
       * 
       * @see ContactChange
       * @see Contact
       * @param contactChanges the array of ContactChange to convert
       * @return the equivalent Contact
       */
      private Contact convertNativeContactChanges(ContactChange[] contactChanges) {
  
          if (contactChanges == null || contactChanges.length <= 0)
              return null;
  
          final Contact contact = new Contact();
  
          contact.localContactID = contactChanges[0].getInternalContactId();
          // coming from native
          contact.nativeContactId = new Integer((int)contactChanges[0].getNabContactId());
          contact.synctophone = true;
  
          // fill the contact with all the details
          for (int i = 0; i < contactChanges.length; i++) {
  
              final ContactDetail detail = convertContactChange(contactChanges[i]);
              // setting it to -1 means that it does not need to be synced back to
              // native
              detail.syncNativeContactId = -1;
              contact.details.add(detail);
          }
  
          return contact;
      }
  
      /**
       * Converts a ContactChange object into an equivalent ContactDetail object.
       * 
       * @see ContactChange
       * @see ContactDetail
       * @param change the ContactChange to convert
       * @return the equivalent ContactDetail
       */
      public ContactDetail convertContactChange(ContactChange change) {
  
          final ContactDetail detail = new ContactDetail();
          final int flag = change.getFlags();
          // conversion is not straightforward, needs a little tweak
          final int key = ContactDetailsTable.mapContactChangeKeyToInternalKey(change.getKey());
  
          detail.localContactID = change.getInternalContactId() != ContactChange.INVALID_ID ? change
                  .getInternalContactId() : null;
          detail.localDetailID = change.getInternalDetailId() != ContactChange.INVALID_ID ? change
                  .getInternalDetailId() : null;
          detail.nativeContactId = change.getNabContactId() != ContactChange.INVALID_ID ? new Integer(
                  (int)change.getNabContactId())
                  : null;
          detail.nativeDetailId = change.getNabDetailId() != ContactChange.INVALID_ID ? new Integer(
                  (int)change.getNabDetailId()) : null;
          detail.unique_id = change.getBackendDetailId() != ContactChange.INVALID_ID ? new Long(
                  change.getBackendDetailId()) : null;
          detail.key = DetailKeys.values()[key];
          detail.keyType = DetailKeyTypes.values()[ContactDetailsTable
                  .mapContactChangeFlagToInternalType(flag)];
          detail.value = change.getValue();
          detail.order = ContactDetailsTable.mapContactChangeFlagToInternalOrder(flag);
  
          return detail;
      }
  
      /**
       * Gets the local IDs of the Contacts that are syncable to native.
       *  
       * @return an array of local contact IDs
       */
      public long[] getNativeSyncableContactsLocalIds() {
  
          long[] ids = null;
          Cursor cursor = null;
  
          try {
  
              final int LOCAL_ID_INDEX = 0;
              final SQLiteDatabase readableDb = getReadableDatabase();
  
              cursor = readableDb.rawQuery(QUERY_NATIVE_SYNCABLE_CONTACTS_LOCAL_IDS, null);
  
              if (cursor.getCount() > 0) {
  
                  int i = 0;
                  ids = new long[cursor.getCount()];
  
                  while (cursor.moveToNext()) {
                      ids[i++] = cursor.getInt(LOCAL_ID_INDEX);
                  }
              } else {
  
                  return null;
              }
          } catch (Exception e) {
  
              if (Settings.ENABLED_DATABASE_TRACE) {
                  DatabaseHelper.trace(true, "getModifiedContactsNativeIds(): " + e);
              }
          } finally {
  
              CloseUtils.close(cursor);
              cursor = null;
          }
  
          return ids;
      }
  
      /**
       * Sets the picture loaded flag and fires a databaseChanged event.
       * 
       * @param localContactId Local contact id of the contact where to set the
       *            flag
       * @param value Value of the flag
       * @return true in case everything went fine, false otherwise
       */
      public final boolean modifyPictureLoadedFlag(final Long localContactId, final Boolean value) {
          ServiceStatus serviceStatus = ContactSummaryTable.modifyPictureLoadedFlag(localContactId,
                  value, getWritableDatabase());
          if (ServiceStatus.SUCCESS != serviceStatus) {
              return false;
          }
          fireDatabaseChangedEvent(DatabaseChangeType.CONTACTS, true);
          return true;
      }
      
      /**
       * This API checks if the thumbnail is downloaded for the contact or not.
       * 
       * @param localContactId the contactId for which a check needs to be done if
       *            the thumbnail is loaded or not
       * @return true if the thumbnail is downloaded for the contact.
       */
      public boolean isPictureLoaded(final Long localContactId) {
          if(localContactId == null) {
              return false;
          }
          boolean isPictureLoaded = false;
          Cursor cr = null;
          final SQLiteDatabase db = getReadableDatabase();
          StringBuffer query = StringBufferPool.getStringBuffer(SQLKeys.SELECT);
          query.append(ContactSummaryTable.Field.PICTURELOADED.toString()).append(SQLKeys.FROM)
                  .append(ContactSummaryTable.TABLE_NAME).append(SQLKeys.WHERE).append(
                          ContactSummaryTable.Field.LOCALCONTACTID.toString()).append(SQLKeys.EQUALS)
                  .append(localContactId);
          try {
              cr = db.rawQuery(StringBufferPool.toStringThenRelease(query), null);
              
              if (cr.moveToFirst()
                      && !cr.isNull(cr.getColumnIndexOrThrow(ContactSummaryTable.Field.PICTURELOADED
                              .toString()))) {
                  int picLoaded = cr.getInt(cr
                          .getColumnIndexOrThrow(ContactSummaryTable.Field.PICTURELOADED.toString()));
                  isPictureLoaded = picLoaded > 0 ? true : false;
              }
          } catch (SQLiteException e) {
              LogUtils.logE("DatabaseHelper.isPictureLoaded() exception", e);
          } finally {
              CloseUtils.close(cr);
          }
          return isPictureLoaded;
      }
      
      /**
       * This method updates the timeline entries.
       * for the contact when new Phone number is added.
       * @param oldPhoneNumber Phone number for which timeline entries
       * need to be updated.
       * @param localContactID Given contact ID
       *
       */
  
           public final void updateTimelineForPhoneNumberChange(
          		           final String oldPhoneNumber,
  			               final Long localContactID) {
  		final SQLiteDatabase db = getWritableDatabase();
  		ActivitiesTable.updateTimelineForPhoneNumberChange(
                                                    oldPhoneNumber,
                                                    localContactID,
  				                                  db);
  	}
  
  	/**
       * This method updates the timeline event for the contact for the provided.
       * Phone number.This function separates the deleted phone number entry
       * @param oldPhoneNumber Phone number for which timeline entries need to be separated.
       * @param localContactID Given contact ID.
       */
  
  	public final void updateTimelineForPhoneNumberDeletion(
                                              final String oldPhoneNumber,
  			                                final Long localContactID) {
  		             final SQLiteDatabase db = getWritableDatabase();
  		             ActivitiesTable.updateTimelineForPhoneNumberDeletion(
  		            		                    oldPhoneNumber,
  		            		                    localContactID,
  		            		                    db);
  	}
  }
  
  