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.tables;
  
  import java.util.ArrayList;
  import java.util.List;
  import android.content.ContentValues;
  import android.database.Cursor;
  import android.database.DatabaseUtils;
  import android.database.SQLException;
  import android.database.sqlite.SQLiteDatabase;
  import android.database.sqlite.SQLiteException;
  import android.database.sqlite.SQLiteStatement;
  import android.text.TextUtils;
  import com.vodafone360.people.Settings;
  import com.vodafone360.people.database.DatabaseHelper;
  import com.vodafone360.people.database.SQLKeys;
  import com.vodafone360.people.database.DatabaseHelper.ServerIdInfo;
  import com.vodafone360.people.database.tables.ContactsTable.ContactIdInfo;
  import com.vodafone360.people.datatypes.Contact;
  import com.vodafone360.people.datatypes.ContactDetail;
  import com.vodafone360.people.datatypes.VCardHelper;
  import com.vodafone360.people.datatypes.ContactDetail.DetailKeyTypes;
  import com.vodafone360.people.datatypes.ContactDetail.DetailKeys;
  import com.vodafone360.people.datatypes.VCardHelper.Name;
  import com.vodafone360.people.engine.contactsync.ContactChange;
  import com.vodafone360.people.service.ServiceStatus;
  import com.vodafone360.people.utils.CloseUtils;
  import com.vodafone360.people.utils.CursorUtils;
  import com.vodafone360.people.utils.LogUtils;
  import com.vodafone360.people.utils.StringBufferPool;
  import com.vodafone360.people.utils.VersionUtils;
  
  /**
   * Provides a wrapper for all the operations on the Contact Details Table. Class
   * is never instantiated, all methods and fields are static.
   */
  public abstract class ContactDetailsTable {
      /**
       * Name of the table in the people database.
       */
      public static final String TABLE_NAME = "ContactDetails";
      
      /**
       * SELECT LocalContactId, DetailLocalId, NativeDetailId, NativeContactIdDup, Key, Type, StringVal, OrderNo FROM ContactDetails.
       * 
       * @see #getContactChanges(long, SQLiteDatabase)
       */
      private final static String QUERY_CONTACT_DETAILS_BY_LOCAL_ID = "SELECT " + Field.LOCALCONTACTID + ", " + Field.DETAILLOCALID + ", " + Field.NATIVEDETAILID +
                                              ", " + Field.NATIVECONTACTID + ", " + Field.TYPE + ", " + Field.ORDER + ", " + Field.KEY +
                                              ", " + Field.STRINGVAL + ", " + Field.DETAILSERVERID + " FROM " + TABLE_NAME + " WHERE " + Field.LOCALCONTACTID + " = ?";
      
      /**
       * 
       */
      private final static String QUERY_NATIVE_SYNCABLE_CONTACT_DETAILS_BY_LOCAL_ID = QUERY_CONTACT_DETAILS_BY_LOCAL_ID
                                                                                    + " AND (" + Field.NATIVESYNCCONTACTID + " IS NULL OR " + Field.NATIVESYNCCONTACTID
                                                                                    + " <> -1)";
      
      
      /**
       * SELECT DISTINCT LocalId
       * FROM ContactDetails
       * WHERE NativeSyncId is NULL or NativeSyncId <> -1
       */
      public final static String QUERY_NATIVE_SYNCABLE_CONTACTS_LOCAL_IDS = "SELECT " + Field.LOCALCONTACTID + " FROM " + TABLE_NAME
                                                                          + " WHERE (" + Field.NATIVESYNCCONTACTID + " IS NULL OR " + Field.NATIVESYNCCONTACTID + " <> -1)";
      
      /**
       * SELECT DetailLocalId, DetailServerId
       * FROM ContactDetails
       * WHERE LocalContactId = ?
       */
      public final static String QUERY_DETAIL_LOCAL_AND_SERVER_IDS_BY_LOCAL_CONTACT_ID = 
                      "SELECT " + Field.DETAILLOCALID + ", "+ Field.DETAILSERVERID + " FROM " + TABLE_NAME + " WHERE "
                      + Field.LOCALCONTACTID + " = ?";
      
      /**
       * SELECT 
       * DetailLocalId, Key, 
       * DetailServerId, NativeContactIdDup, 
       * NativeDetailId, NativeValue1,
       * NativeValue2, NativeValue3,
       * NativeSyncContactId
       * FROM ContactDetails
       * WHERE LocalContactId = ?
       */
      private static final String QUERY_DETAIL_BY_LOCAL_CONTACT_ID = 
                      "SELECT " + Field.DETAILLOCALID + "," + Field.KEY + ","
                      + Field.DETAILSERVERID + "," + Field.NATIVECONTACTID + ","
                      + Field.NATIVEDETAILID + "," + Field.NATIVEDETAILVAL1 + ","
                      + Field.NATIVEDETAILVAL2 + "," + Field.NATIVEDETAILVAL3 + ","
                      + Field.NATIVESYNCCONTACTID + " FROM " + TABLE_NAME + " WHERE "
                      + Field.LOCALCONTACTID + "=?";
      
      /**
       * DetailLocalId Column for QUERY_DETAIL_BY_LOCAL_CONTACT_ID query
       */
      final static int QUERY_COLUMN_LOCALDETAILID = 0;
      /**
       * Key Column for QUERY_DETAIL_BY_LOCAL_CONTACT_ID query
       */
      final static int QUERY_COLUMN_KEY = 1;
      /**
       * DetailServerId Column for QUERY_DETAIL_BY_LOCAL_CONTACT_ID query
       */
      final static int QUERY_COLUMN_SERVERDETAILID = 2;
      /**
       * NativeContactIdDup Column for QUERY_DETAIL_BY_LOCAL_CONTACT_ID query
       */
      final static int QUERY_COLUMN_NATIVECONTACTID = 3;
      /**
       * NativeDetailId Column for QUERY_DETAIL_BY_LOCAL_CONTACT_ID query
       */
      final static int QUERY_COLUMN_NATIVEDETAILID = 4;
      /**
       * NativeValue1 Column for QUERY_DETAIL_BY_LOCAL_CONTACT_ID query
       */
      final static int QUERY_COLUMN_NATIVEVAL1 = 5;
      /**
       * NativeValue2 Column for QUERY_DETAIL_BY_LOCAL_CONTACT_ID query
       */
      final static int QUERY_COLUMN_NATIVEVAL2 = 6;
      /**
       * NativeValue3 Column for QUERY_DETAIL_BY_LOCAL_CONTACT_ID query
       */
      final static int QUERY_COLUMN_NATIVEVAL3 = 7;
      /**
       * NativeSyncContactId Column for QUERY_DETAIL_BY_LOCAL_CONTACT_ID query
       */
      final static int QUERY_COLUMN_NATIVESYNCCONTACTID = 8;
  
      /**
       * Equals the number of comma separated items returned by
       * {@link #getFullQueryList()}.
       * 
       * @See {@link #getQueryDataLength()}
       */
      private static final int DATA_QUERY_LENGTH = 14;
      
      /**
       * Associates a constant with a field string in the People database.
       */
      public static enum Field {
          DETAILLOCALID("DetailLocalId"),
          DETAILSERVERID("DetailServerId"),
          KEY("Key"),
          TYPE("Type"),
          STRINGVAL("StringVal"),
          ALT("Alt"),
          DELETED("Deleted"),
          ORDER("OrderNo"),
          BYTES("Bytes"),
          BYTESMIMETYPE("BytesMimeType"),
          PHOTOURL("PhotoUrl"),
          UPDATED("Updated"),
          LOCALCONTACTID("LocalContactId"),
          NATIVECONTACTID("NativeContactIdDup"),
          NATIVEDETAILID("NativeDetailId"),
          NATIVEDETAILVAL1("NativeValue1"),
          NATIVEDETAILVAL2("NativeValue2"),
          NATIVEDETAILVAL3("NativeValue3"),
          SERVERSYNCCONTACTID("ServerSyncContactId"),
          NATIVESYNCCONTACTID("NativeSyncContactId");
          /**
           * Name of the table field as it appears in the database.
           */
          private final String mField;
  
          /**
           * Constructs the enumeration value by associating it with a string.
           * 
           * @param field The string specified in the list above.
           */
          private Field(String field) {
              mField = field;
          }
  
          /**
           * Returns the enum value as the associated field name.
           */
          public String toString() {
              return mField;
          }
      }
  
      /**
       * Holds the Native Contact information that is stored in the database for a
       * contact detail. Information is used when matching a contact detail from
       * People with a detail in the Android native phonebook.
       */
      public static class NativeIdInfo {
          /**
           * Associated with the primary key (localDetailId) in the People Contact
           * Details table.
           */
          public long localId;
  
          /**
           * Associated with the primary key (_id) in the native People table.
           */
          public Integer nativeContactId;
  
          /**
           * Associated with the primary key (_id) in the native Phones,
           * ContactMethods or Organizations table (depending on contact detail).
           * Can be null for some types of detail.
           */
          public Integer nativeDetailId;
  
          /**
           * Detail type specific. Stores value of one of the fields in a native
           * table (Phones, ContactMethods or Organisations). Used to determine if
           * the detail in the native table has changed. Examples: 1) In case of
           * phone number, this value holds the phone number in the same format as
           * the native database. 2) In case of address, this value holds the full
           * address (all in one string). This differs from the value field which
           * stores the address in VCard format.
           */
          public String nativeVal1;
  
          /**
           * Detail type specific. Stores value of one of the fields in a native
           * table (Phones, ContactMethods or Organisations). Used to determine if
           * the detail in the native table has changed.
           */
          public String nativeVal2;
  
          /**
           * Detail type specific. Stores value of one of the fields in a native
           * table (Phones, ContactMethods or Organisations). Used to determine if
           * the detail in the native table has changed.
           */
          public String nativeVal3;
  
          /**
           * Can hold one of the following values:
           * <ul>
           * <li><b>NULL</b> Contact has just been added to the database</li>
           * <li><b>-1</b> Detail in People database has been synced with the
           * native</li>
           * <li><b>Positive Integer</b> Detail has been added or changed and
           * needs to be synced to the native.</li>
           * </ul>
           */
          public long syncNativeContactId = -1L;
      }
  
      /**
       * Creates the contact detail table.
       * 
       * @param writeableDb The SQLite database
       * @throws SQLException If the table already exists or the database is
       *             corrupt
       */
      public static void create(SQLiteDatabase writeableDb) throws SQLException {
          DatabaseHelper.trace(true, "ContactDetailsTable.create()");
          writeableDb.execSQL("CREATE TABLE " + TABLE_NAME + " (" + Field.DETAILLOCALID
                  + " INTEGER PRIMARY KEY AUTOINCREMENT, " + Field.DETAILSERVERID + " LONG, "
                  + Field.KEY + " INTEGER, " + Field.TYPE + " INTEGER, " + Field.STRINGVAL
                  + " TEXT, " + Field.ALT + " TEXT, " + Field.DELETED + " BOOLEAN, " + Field.ORDER
                  + " INTEGER, " + Field.BYTES + " BINARY, " + Field.BYTESMIMETYPE + " TEXT, "
                  + Field.PHOTOURL + " TEXT, " + Field.UPDATED + " INTEGER, " + Field.LOCALCONTACTID
                  + " LONG NOT NULL, " + Field.NATIVECONTACTID + " INTEGER, " + Field.NATIVEDETAILID
                  + " INTEGER," + Field.NATIVEDETAILVAL1 + " TEXT," + Field.NATIVEDETAILVAL2
                  + " TEXT," + Field.NATIVEDETAILVAL3 + " TEXT," + Field.SERVERSYNCCONTACTID
                  + " LONG," + Field.NATIVESYNCCONTACTID + " INTEGER);");
      }
  
      /**
       * Fetches the list of table fields that can be injected into an SQL query
       * statement. The {@link #getQueryData(Cursor)} method can be used to obtain
       * the data from the query.
       * 
       * @return The query string
       * @Note Constant {@link #DATA_QUERY_LENGTH} must equal the number of comma
       *       separated items returned by this function.
       */
      private static String getFullQueryList() {
          return Field.DETAILLOCALID + ", " + Field.DETAILSERVERID + ", " + Field.LOCALCONTACTID
                  + ", " + Field.KEY + ", " + Field.TYPE + ", " + Field.STRINGVAL + ", " + Field.ALT
                  + ", " + Field.ORDER + ", " + Field.UPDATED + ", " + Field.NATIVECONTACTID + ", "
                  + Field.NATIVEDETAILID + ", " + Field.NATIVEDETAILVAL1 + ", "
                  + Field.NATIVEDETAILVAL2 + ", " + Field.NATIVEDETAILVAL3 + ", "
                  + Field.SERVERSYNCCONTACTID + ", " + Field.NATIVESYNCCONTACTID;
      }
  
      /**
       * Returns the SQL for doing a raw query on the contact details table. The
       * {@link #getQueryData(Cursor)} method can be used for fetching the data
       * from the resulting cursor.
       * 
       * @param whereClause The SQL constraint (cannot be empty or null)
       * @return The SQL query string
       */
      public static String getQueryStringSql(String whereClause) {
          return "SELECT " + getFullQueryList() + " FROM " + TABLE_NAME + " WHERE " + whereClause;
      }
  
      /**
       * Returns the number of comma separated items returned by
       * {@link #getFullQueryList()}.
       * 
       * @See {@link #DATA_QUERY_LENGTH}
       * @return The number of items
       */
      private static int getQueryDataLength() {
          return DATA_QUERY_LENGTH;
      }
  
      /**
       * Identifies the columns of the data returned by getFullQueryList()
       */
      private static final int COLUMN_LOCALDETAILID = 0;
  
      private static final int COLUMN_SERVERDETAILID = 1;
  
      private static final int COLUMN_LOCALCONTACTID = 2;
  
      private static final int COLUMN_KEY = 3;
  
      private static final int COLUMN_KEYTYPE = 4;
  
      private static final int COLUMN_VALUE = 5;
  
      private static final int COLUMN_ALT = 6;
  
      private static final int COLUMN_ORDER = 7;
  
      private static final int COLUMN_UPDATED = 8;
  
      private static final int COLUMN_NATIVECONTACTID = 9;
  
      private static final int COLUMN_NATIVEDETAILID = 10;
  
      private static final int COLUMN_NATIVEVAL1 = 11;
  
      private static final int COLUMN_NATIVEVAL2 = 12;
  
      private static final int COLUMN_NATIVEVAL3 = 13;
  
      private static final int COLUMN_SERVER_SYNC = 14;
  
      private static final int COLUMN_NATIVE_SYNC_CONTACT_ID = 15;
  
      /**
       * Returns the contact detail for the current record in the cursor. Query to
       * produce the cursor must be obtained using the {@link #getFullQueryList()}
       * .
       * 
       * @param The cursor obtained from doing the query
       * @return The contact detail object.
       */
      public static ContactDetail getQueryData(Cursor c) {
          ContactDetail detail = new ContactDetail();
          if (!c.isNull(COLUMN_LOCALDETAILID)) {
              detail.localDetailID = c.getLong(COLUMN_LOCALDETAILID);
          }
          if (!c.isNull(COLUMN_SERVERDETAILID)) {
              detail.unique_id = c.getLong(COLUMN_SERVERDETAILID);
          }
          if (!c.isNull(COLUMN_LOCALCONTACTID)) {
              detail.localContactID = c.getLong(COLUMN_LOCALCONTACTID);
          }
          detail.key = ContactDetail.DetailKeys.values()[c.getInt(COLUMN_KEY)];
          if (!c.isNull(COLUMN_KEYTYPE)) {
              detail.keyType = ContactDetail.DetailKeyTypes.values()[c.getInt(COLUMN_KEYTYPE)];
          }
          detail.value = c.getString(COLUMN_VALUE);
          if (!c.isNull(COLUMN_ALT)) {
              detail.alt = c.getString(COLUMN_ALT);
          }
          if (!c.isNull(COLUMN_ORDER)) {
              detail.order = c.getInt(COLUMN_ORDER);
          }
          if (!c.isNull(COLUMN_UPDATED)) {
              detail.updated = c.getLong(COLUMN_UPDATED);
          }
          if (!c.isNull(COLUMN_NATIVECONTACTID)) {
              detail.nativeContactId = c.getInt(COLUMN_NATIVECONTACTID);
          }
          if (!c.isNull(COLUMN_NATIVEDETAILID)) {
              detail.nativeDetailId = c.getInt(COLUMN_NATIVEDETAILID);
          }
          if (!c.isNull(COLUMN_NATIVEVAL1)) {
              detail.nativeVal1 = c.getString(COLUMN_NATIVEVAL1);
          }
          if (!c.isNull(COLUMN_NATIVEVAL2)) {
              detail.nativeVal2 = c.getString(COLUMN_NATIVEVAL2);
          }
          if (!c.isNull(COLUMN_NATIVEVAL3)) {
              detail.nativeVal3 = c.getString(COLUMN_NATIVEVAL3);
          }
          if (!c.isNull(COLUMN_SERVER_SYNC)) {
              detail.serverContactId = c.getLong(COLUMN_SERVER_SYNC);
          }
          if (!c.isNull(COLUMN_NATIVE_SYNC_CONTACT_ID)) {
              detail.syncNativeContactId = c.getInt(COLUMN_NATIVE_SYNC_CONTACT_ID);
          }
          return detail;
      }
  
      /**
       * Extracts the suitable content values from a contact detail which can be
       * used to update or insert a record in the table.
       * 
       * @param detail The contact detail
       * @param syncToServer If true the detail needs to be synced to the server,
       *            otherwise no sync required
       * @param syncToNative If true the detail needs to be synced with the
       *            native, otherwise no sync required
       * @return The ContentValues to use for the update or insert.
       * @note If any given field values are NULL they will NOT be added to the
       *       content values data.
       */
      private static ContentValues fillUpdateData(ContactDetail detail, boolean syncToServer,
              boolean syncToNative) {
          ContentValues contactDetailValues = new ContentValues();
          if (detail.key != null) {
              contactDetailValues.put(Field.KEY.toString(), detail.key.ordinal());
          }
          if (detail.keyType != null) {
              contactDetailValues.put(Field.TYPE.toString(), detail.keyType.ordinal());
          }
          if (detail.localDetailID != null) {
              contactDetailValues.put(Field.DETAILLOCALID.toString(), detail.localDetailID);
          }
          contactDetailValues.put(Field.STRINGVAL.toString(), detail.value);
          if (detail.alt != null
                  || (detail.key != null && detail.key == ContactDetail.DetailKeys.PRESENCE_TEXT)) {
              contactDetailValues.put(Field.ALT.toString(), detail.alt);
          }
          if (detail.unique_id != null) {
              contactDetailValues.put(Field.DETAILSERVERID.toString(), detail.unique_id);
          }
          if (detail.order != null) {
              contactDetailValues.put(Field.ORDER.toString(), detail.order);
          }
          if (detail.updated != null) {
              contactDetailValues.put(Field.UPDATED.toString(), detail.updated);
          }
          contactDetailValues.put(Field.LOCALCONTACTID.toString(), detail.localContactID);
          if (detail.deleted != null) {
              contactDetailValues.put(Field.DELETED.toString(), false);
          }
          // contactDetailValues.put(Field.BYTES.toString(), (byte[])null); //
          // TODO: Needs implementation
          if (detail.photo_mime_type != null) {
              contactDetailValues.put(Field.BYTESMIMETYPE.toString(), detail.photo_mime_type);
          }
          if (detail.photo_url != null) {
              contactDetailValues.put(Field.PHOTOURL.toString(), detail.photo_url);
          }
          if (detail.nativeContactId != null) {
              contactDetailValues.put(Field.NATIVECONTACTID.toString(), detail.nativeContactId);
          }
          if (detail.nativeDetailId != null) {
              contactDetailValues.put(Field.NATIVEDETAILID.toString(), detail.nativeDetailId);
          }
          if (syncToServer) {
              contactDetailValues.put(Field.SERVERSYNCCONTACTID.toString(), detail.serverContactId);
          } else {
              contactDetailValues.put(Field.SERVERSYNCCONTACTID.toString(), -1);
          }
          if (syncToNative) {
              contactDetailValues.put(Field.NATIVESYNCCONTACTID.toString(),
                      detail.syncNativeContactId);
          } else {
              contactDetailValues.put(Field.NATIVESYNCCONTACTID.toString(), -1);
              if (detail.nativeVal1 != null) {
                  contactDetailValues.put(Field.NATIVEDETAILVAL1.toString(), detail.nativeVal1);
              }
              if (detail.nativeVal2 != null) {
                  contactDetailValues.put(Field.NATIVEDETAILVAL2.toString(), detail.nativeVal2);
              }
              if (detail.nativeVal3 != null) {
                  contactDetailValues.put(Field.NATIVEDETAILVAL3.toString(), detail.nativeVal3);
              }
          }
          return contactDetailValues;
      }
  
      /**
       * Fetches a contact detail from the table
       * 
       * @param localDetailId The local ID of the required detail
       * @param readableDb The readable SQLite database object
       * @return ContactDetail object or NULL if the detail was not found
       */
      public static ContactDetail fetchDetail(long localDetailId, SQLiteDatabase readableDb) {
          String[] mArgs = {
              String.format("%d", localDetailId)
          };
          ContactDetail detail = null;
          Cursor cursor = null;
          try {
              cursor = readableDb.rawQuery(getQueryStringSql(Field.DETAILLOCALID + " = ?"), mArgs);
              if (cursor.moveToFirst()) {
                  detail = getQueryData(cursor);
              }
          } catch (SQLiteException e) {
              LogUtils.logE(
                      "ContactDetailsTable.fetchDetail() Exception - Unable to fetch contact detail",
                      e);
              return null;
          } finally {
              CloseUtils.close(cursor);
              cursor = null;
          }
  
          if (Settings.ENABLED_DATABASE_TRACE) {
              DatabaseHelper.trace(false, "ContactDetailsTable.fetchDetail() localDetailId["
                      + localDetailId + "] unique_id[" + detail.unique_id + "] value[" + detail.value
                      + "]");
          }
          return detail;
      }
  
      /**
       * Removes a contact detail from the table
       * 
       * @param localDetailId The local ID identifying the detail
       * @param writeableDb A writable SQLite database object.
       * @return true if the delete was successful, false otherwise
       */
      public static boolean deleteDetailByDetailId(long localDetailId, SQLiteDatabase writeableDb) {
          try {
              String[] mArgs = {
                  String.format("%d", localDetailId)
              };
              writeableDb.delete(TABLE_NAME, Field.DETAILLOCALID + "=?", mArgs);
              DatabaseHelper.trace(true,
                      "ContactDetailsTable.deleteDetailByDetailId() Deleted localDetailId["
                              + localDetailId + "]");
              return true;
  
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "ContactDetailsTable.fetchDetail() SQLException - Unable to delete contact detail with localDetailId["
                                      + localDetailId + "]", e);
              return false;
          }
      }
  
      /**
       * Deletes all the contact details associated with a contact
       * 
       * @param localContactId The local contact ID identifying the contact
       * @param writeableDb A writable SQLite database object.
       * @return true if the delete was successful, false otherwise
       */
      public static ServiceStatus deleteDetailByContactId(long localContactId,
              SQLiteDatabase writeableDb) {
          try {
              String[] args = {
                  String.format("%d", localContactId)
              };
              writeableDb.delete(TABLE_NAME, Field.LOCALCONTACTID + "=?", args);
              DatabaseHelper.trace(true,
                      "ContactDetailsTable.deleteDetailByContactId() Deleted localContactId["
                              + localContactId + "]");
              return ServiceStatus.SUCCESS;
  
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "ContactDetailsTable.deleteDetailByContactId() SQLException - Unable to delete contact detail with localContactId["
                                      + localContactId + "]", e);
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          }
      }
  
      /**
       * Adds a new contact detail to the table.
       * 
       * @param detail The new detail
       * @param syncToServer Mark the new detail so it will be synced to the
       *            server
       * @param syncToNative Mark the new detail so it will be synced to the
       *            native database
       * @param writeableDb A writable SQLite database object.
       * @return SUCCESS or a suitable error code.
       */
      public static ServiceStatus addContactDetail(ContactDetail detail, boolean syncToServer,
              boolean syncToNative, SQLiteDatabase writeableDb) {
          try {
              if (detail.localContactID == null) {
                  LogUtils
                          .logE("ContactDetailsTable.addContactDetail() Unable to add contact detail - invalid parameter");
                  return ServiceStatus.ERROR_NOT_FOUND;
              }
              detail.localDetailID = null;
              ContentValues cv = fillUpdateData(detail, syncToServer, syncToNative);
              detail.localDetailID = writeableDb.insertOrThrow(TABLE_NAME, null, cv);
              if (detail.localDetailID < 0) {
                  LogUtils
                          .logE("ContactDetailsTable.addContactDetail() Unable to add contact detail");
                  return ServiceStatus.ERROR_DATABASE_CORRUPT;
              }
              DatabaseHelper.trace(true,
                      "ContactDetailsTable.addContactDetail() Added localDetailID["
                              + detail.localDetailID + "]");
              return ServiceStatus.SUCCESS;
  
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "ContactDetailsTable.addContactDetail() SQLException - Unable to add contact detail",
                              e);
  
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          }
      }
  
      /**
       * Updates an existing contact detail in the table.
       * 
       * @param detail The modified detail.
       * @param syncToServer Mark the new detail so it will be synced to the
       *            server
       * @param syncToNative Mark the new detail so it will be synced to the
       *            native database
       * @param writeableDb A writable SQLite database object.
       * @return SUCCESS or a suitable error code.
       * @note If any given field values in the contact detail are NULL they will
       *       NOT be modified in the database.
       */
      public static ServiceStatus modifyDetail(ContactDetail detail, boolean syncToServer,
              boolean syncToNative, SQLiteDatabase writeableDb) {
          try { 
              
              // Sometimes we get surname duplicated in first name (Max Mustermann
              // Mustermann) on a VCARD_NAME
              // To fix this we will remove surnames from first names and sync
              // back to server if we change anything
              if (VersionUtils.is2XPlatform() == false && detail.key == DetailKeys.VCARD_NAME) {
                  Name name = VCardHelper.getName(detail.value);
                  if (name.surname.length() > 0) {
                      name.firstname = name.firstname.replace(name.surname, "").trim();
                      name.midname = name.midname.replace(name.surname, "").trim();
                  }
                  String vcardName = VCardHelper.makeName(name);
                  if (!detail.value.equals(vcardName)) {
                      syncToServer = true;
                      detail.value = vcardName;
                  }
  
              }
              
              ContentValues contactDetailValues = fillUpdateData(detail, syncToServer, syncToNative);
              if (writeableDb.update(TABLE_NAME, contactDetailValues, Field.DETAILLOCALID + " = "
                      + detail.localDetailID, null) <= 0) {
                  LogUtils
                          .logE("ContactDetailsTable.modifyDetail() Unable to update contact detail , localDetailID["
                                  + detail.localDetailID + "]");
                  return ServiceStatus.ERROR_NOT_FOUND;
              }
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "ContactDetailsTable.modifyDetail() SQLException - Unable to modify contact detail",
                              e);
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          }
          DatabaseHelper.trace(true, "ContactDetailsTable.modifyDetail() localContactID["
                  + detail.localContactID + "] localDetailID[" + detail.localDetailID + "]");
          return ServiceStatus.SUCCESS;
      }
  
      /**
       * Updates the detail server ID stored with the record and
       * sets the detail as synchronized with the server.
       * 
       * @param localId The local detail ID identifying the record
       * @param serverId The new server ID
       * @param writeableDb A writable SQLite database object.
       * @return true if the update was successful, false otherwise
       */
      public static boolean syncSetServerId(long localId, Long serverId,
              SQLiteDatabase writeableDb) {
          DatabaseHelper.trace(true, "ContactDetailsTable.modifyDetailServerId()");
          try {
              final ContentValues cv = new ContentValues();
              cv.put(Field.DETAILSERVERID.toString(), serverId);
              cv.put(Field.SERVERSYNCCONTACTID.toString(), -1); // flag the detail as synchronized with the server
              String[] args = {
                  String.format("%d", localId)
              };
              if (writeableDb.update(TABLE_NAME, cv, Field.DETAILLOCALID + "=?", args) <= 0) {
                  LogUtils
                          .logE("ContactDetailsTable.modifyDetailServerId() Unable to update contact detail server ID");
                  return false;
              }
              return true;
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "ContactDetailsTable.modifyDetailServerId() SQLException - Unable to modify contact detail server ID",
                              e);
              return false;
          }
      }
  
      /**
       * Updates the detail native contact information stored with the record
       * 
       * @param detail The contact detail which contacts the new native
       *            information and the local ID of the detail to be modified.
       * @param writeableDb A writable SQLite database object.
       * @return true if the update was successful, false otherwise
       */
      public static boolean modifyDetailNativeId(ContactDetail detail, SQLiteDatabase writeableDb) {
          DatabaseHelper.trace(true, "ContactDetailsTable.modifyDetailNativeId()");
          try {
              ContentValues cv = new ContentValues();
              cv.put(Field.NATIVECONTACTID.toString(), detail.nativeContactId);
              cv.put(Field.NATIVEDETAILID.toString(), detail.nativeDetailId);
              cv.put(Field.NATIVEDETAILVAL1.toString(), detail.nativeVal1);
              cv.put(Field.NATIVEDETAILVAL2.toString(), detail.nativeVal2);
              cv.put(Field.NATIVEDETAILVAL3.toString(), detail.nativeVal3);
              String[] args = {
                  String.format("%d", detail.localDetailID)
              };
              if (writeableDb.update(TABLE_NAME, cv, Field.DETAILLOCALID + "=?", args) <= 0) {
                  LogUtils
                          .logE("ContactDetailsTable.modifyDetailNativeId() Unable to update contact detail native ID");
                  return false;
              }
              return true;
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "ContactDetailsTable.modifyDetailNativeId() SQLException - Unable to modify contact detail native ID",
                              e);
              return false;
          }
      }
  
      /**
       * Fetches all contact details that need to be synced with the native
       * contacts database
       * 
       * @param detailList A list that will be populated with the contact details.
       * @param keyList A list of keys to filter the result
       * @param byDetailId true to order the details by native detail ID, false to
       *            order by native contact ID
       * @param firstIndex The index of the first record to fetch
       * @param count The number of records to fetch (or -1 to fetch all)
       * @param readableDb A readable SQLite database object.
       * @return true if the operation was successful, false otherwise
       */
      public static boolean fetchContactDetailsForNative(List<ContactDetail> detailList,
              DetailKeys[] keyList, boolean byDetailId, int firstIndex, int count,
              SQLiteDatabase readableDb) {
          DatabaseHelper.trace(false, "ContactDetailsTable.fetchContactDetailsForNative()");
          detailList.clear();
          Cursor c = null;
          try {
              StringBuilder sb1 = new StringBuilder();
              for (int i = 0; i < keyList.length; i++) {
                  sb1.append(Field.KEY + " = " + keyList[i].ordinal());
                  if (i < keyList.length - 1) {
                      sb1.append(" OR ");
                  }
              }
              String orderByText = null;
              if (byDetailId) {
                  orderByText = Field.NATIVEDETAILID.toString();
              } else {
                  orderByText = Field.NATIVECONTACTID.toString();
              }
              c = readableDb.rawQuery("SELECT " + getFullQueryList() + ", "
                      + Field.NATIVESYNCCONTACTID + " FROM " + TABLE_NAME + " WHERE "
                      + Field.NATIVECONTACTID + " IS NOT NULL AND (" + sb1 + ") ORDER BY "
                      + orderByText + " LIMIT " + firstIndex + "," + count, null);
              while (c.moveToNext()) {
                  ContactDetail detail = getQueryData(c);
                  final int fieldIdx = getQueryDataLength();
                  if (!c.isNull(fieldIdx)) {
                      detail.syncNativeContactId = c.getInt(fieldIdx);
                  }
                  detailList.add(detail);
              }
              return true;
          } catch (SQLException e) {
              return false;
          } finally {
              CloseUtils.close(c);
          }
      }
  
      /**
       * Fetches the first contact detail found for a contact and key.
       * 
       * @param localContactId The local contact ID
       * @param key The contact detail key value
       * @param readableDb A readable SQLite database object.
       * @return The contact detail, or NULL if it could not be found.
       */
      public static ContactDetail fetchDetail(long localContactId, DetailKeys key,
              SQLiteDatabase readableDb) {
          DatabaseHelper.trace(false, "ContactDetailsTable.fetchDetail()");
          String[] args = {
                  String.format("%d", localContactId), String.format("%d", key.ordinal())
          };
          ContactDetail detail = null;
          Cursor c = null;
          try {
              c = readableDb.rawQuery(getQueryStringSql(Field.LOCALCONTACTID + "=? AND " + Field.KEY
                      + "=?"), args);
              if (c.moveToFirst()) {
                  detail = getQueryData(c);
              }
          } catch (SQLiteException e) {
              LogUtils.logE(
                      "ContactDetailsTable.fetchDetail() Exception - Unable to fetch contact detail",
                      e);
              return null;
          } finally {
              CloseUtils.close(c);
              c = null;
          }
          return detail;
      }
  
      /**
       * Finds a phone contact detail which matches a given telephone number. Uses
       * the native Android functionality for matching the numbers.
       * 
       * @param phoneNumber The number to find
       * @param phoneDetail An empty contact detail where the resulting phone
       *            contact detail will be stored.
       * @param nameDetail An empty contact detail where the resulting name
       *            contact detail will be stored.
       * @param readableDb A readable SQLite database object.
       * @return SUCCESS or a suitable error code.
       */
      public static ServiceStatus fetchContactInfo(String phoneNumber, ContactDetail phoneDetail,
              ContactDetail nameDetail, SQLiteDatabase readableDb) {
          DatabaseHelper.trace(false, "ContactDetailsTable.fetchContactInfo() phoneNumber["
                  + phoneNumber + "]");
          if (phoneNumber == null) {
              return ServiceStatus.ERROR_NOT_FOUND;
          }
          Cursor c2 = null;
          final String searchNumber = DatabaseUtils.sqlEscapeString(phoneNumber);
          try {
              String[] args = {
                  String.format("%d", ContactDetail.DetailKeys.VCARD_PHONE.ordinal())
              };
              c2 = readableDb.rawQuery(getQueryStringSql(Field.KEY + "=? AND PHONE_NUMBERS_EQUAL("
                      + Field.STRINGVAL + "," + searchNumber + ")"), args);
              if (!c2.moveToFirst()) {
                  return ServiceStatus.ERROR_NOT_FOUND;
              }
              phoneDetail.copy(getQueryData(c2));
              if (nameDetail != null) {
                  ContactDetail fetchedNameDetail = fetchDetail(phoneDetail.localContactID,
                          ContactDetail.DetailKeys.VCARD_NAME, readableDb);
                  if (fetchedNameDetail != null) {
                      nameDetail.copy(fetchedNameDetail);
                  }
              }
          } catch (SQLiteException e) {
              LogUtils
                      .logE(
                              "ContactDetailsTable.fetchContactInfo() Exception - Unable to fetch contact detail",
                              e);
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          } finally {
              CloseUtils.close(c2);
              c2 = null;
          }
  
          return ServiceStatus.SUCCESS;
      }
  
      /**
       * Fetch details for a given contact
       * 
       * @param localContactId The local ID of the contact
       * @param detailList A list which will be populated with the details
       * @param readableDb A readable SQLite database object.
       * @return SUCCESS or a suitable error code.
       */
      public static ServiceStatus fetchContactDetails(Long localContactId,
              List<ContactDetail> detailList, SQLiteDatabase readableDb) {
          DatabaseHelper.trace(false, "ContactDetailsTable.fetchContactDetails() localContactId["
                  + localContactId + "]");
  
          String[] args = {
              String.format("%d", localContactId)
          };
          Cursor c = null;
          try {
              c = readableDb.rawQuery(ContactDetailsTable
                      .getQueryStringSql(ContactDetailsTable.Field.LOCALCONTACTID + " = ?"), args);
              detailList.clear();
              while (c.moveToNext()) {
                  detailList.add(ContactDetailsTable.getQueryData(c));
              }
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "ContactDetailsTable.fetchContactDetails() Exception - Unable to fetch contact details for contact",
                              e);
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          } finally {
              CloseUtils.close(c);
          }
          return ServiceStatus.SUCCESS;
      }
  
      /**
       * Set contact detail server ID for all those details which require a server
       * ID. In any case, the server sync contact ID flag is set to -1 to indicate
       * that the detail has been fully synced with the server.
       * 
       * @param serverIdList The list of contact details. This list should include
       *            all details even the ones which don't have server IDs.
       * @param writableDb A writable SQLite database object
       * @return SUCCESS or a suitable error code.
       */
      public static ServiceStatus syncSetServerIds(List<ServerIdInfo> serverIdList,
              SQLiteDatabase writableDb) {
          final int STATEMENT1_COLUMN_SERVERID = 1;
          final int STATEMENT1_COLUMN_LOCALID = 2;
          final int STATEMENT2_COLUMN_LOCALID = 1;
  
          DatabaseHelper.trace(true, "ContactDetailsTable.syncSetServerIds()");
          if (serverIdList.size() == 0) {
              return ServiceStatus.SUCCESS;
          }
          
          SQLiteStatement statement1 = null;
          SQLiteStatement statement2 = null;
          try {
              writableDb.beginTransaction();
              for (int i = 0; i < serverIdList.size(); i++) {
                  final ServerIdInfo info = serverIdList.get(i);
                  if (info.serverId != null) {
                      if (statement1 == null) {
                          statement1 = writableDb.compileStatement("UPDATE " + TABLE_NAME + " SET "
                                  + Field.DETAILSERVERID + "=?," + Field.SERVERSYNCCONTACTID
                                  + "=-1 WHERE " + Field.DETAILLOCALID + "=?");
                      }
                      statement1.bindLong(STATEMENT1_COLUMN_SERVERID, info.serverId);
                      statement1.bindLong(STATEMENT1_COLUMN_LOCALID, info.localId);
                      statement1.execute();
                  } else {
                      if (statement2 == null) {
                          statement2 = writableDb.compileStatement("UPDATE " + TABLE_NAME + " SET "
                                  + Field.SERVERSYNCCONTACTID + "=-1 WHERE " + Field.DETAILLOCALID
                                  + "=?");
                      }
                      statement2.bindLong(STATEMENT2_COLUMN_LOCALID, info.localId);
                      statement2.execute();
                  }
              }
              writableDb.setTransactionSuccessful();
              return ServiceStatus.SUCCESS;
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "ContactDetailsTable.syncSetServerIds() SQLException - Unable to update contact detail server Ids",
                              e);
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          } finally {
              writableDb.endTransaction();
              if(statement1 != null) {
                  statement1.close();
                  statement1 = null;
              }
              if(statement2 != null) {
                  statement2.close();
                  statement2 = null;
              }
          }
      }
  
      /**
       * Set native detail ID for all those details which require an ID. In any
       * case, the native sync contact ID flag is set to -1 to indicate that the
       * detail has been fully synced with the native contacts database.
       * 
       * @param serverIdList The list of contact details. This list should include
       *            all details even the ones which don't have native IDs.
       * @param writableDb A writable SQLite database object
       * @return SUCCESS or a suitable error code.
       */
      public static ServiceStatus syncSetNativeIds(List<NativeIdInfo> nativeIdList,
              SQLiteDatabase writableDb) {
          final int STATEMENT1_COLUMN_NATIVECONTACTID = 1;
          final int STATEMENT1_COLUMN_NATIVEDETAILID = 2;
          final int STATEMENT1_COLUMN_NATIVEVAL1 = 3;
          final int STATEMENT1_COLUMN_NATIVEVAL2 = 4;
          final int STATEMENT1_COLUMN_NATIVEVAL3 = 5;
          final int STATEMENT1_COLUMN_SYNCNATIVECONTACTID = 6;
          final int STATEMENT1_COLUMN_LOCALID = 7;
  
          DatabaseHelper.trace(true, "ContactDetailsTable.syncSetNativeIds()");
          if (nativeIdList.size() == 0) {
              return ServiceStatus.SUCCESS;
          }
          
          try {
              writableDb.beginTransaction();
              SQLiteStatement statement = writableDb.compileStatement("UPDATE " + TABLE_NAME
                      + " SET " + Field.NATIVECONTACTID + "=?," + Field.NATIVEDETAILID + "=?,"
                      + Field.NATIVEDETAILVAL1 + "=?," + Field.NATIVEDETAILVAL2 + "=?,"
                      + Field.NATIVEDETAILVAL3 + "=?," + Field.NATIVESYNCCONTACTID + "=? WHERE "
                      + Field.DETAILLOCALID + "=?");
              for (int i = 0; i < nativeIdList.size(); i++) {
                  final NativeIdInfo info = nativeIdList.get(i);
                  statement.clearBindings();
                  if (info.nativeContactId != null) {
                      statement.bindLong(STATEMENT1_COLUMN_NATIVECONTACTID, info.nativeContactId);
                  }
                  if (info.nativeDetailId != null) {
                      statement.bindLong(STATEMENT1_COLUMN_NATIVEDETAILID, info.nativeDetailId);
                  }
                  if (info.nativeVal1 != null) {
                      statement.bindString(STATEMENT1_COLUMN_NATIVEVAL1, info.nativeVal1);
                  }
                  if (info.nativeVal2 != null) {
                      statement.bindString(STATEMENT1_COLUMN_NATIVEVAL2, info.nativeVal2);
                  }
                  if (info.nativeVal3 != null) {
                      statement.bindString(STATEMENT1_COLUMN_NATIVEVAL3, info.nativeVal3);
                  }
                  statement.bindLong(STATEMENT1_COLUMN_SYNCNATIVECONTACTID, info.syncNativeContactId);
                  statement.bindLong(STATEMENT1_COLUMN_LOCALID, info.localId);
                  statement.execute();
              }
              writableDb.setTransactionSuccessful();
              return ServiceStatus.SUCCESS;
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "ContactDetailsTable.syncSetNativeIds() SQLException - Unable to update contact detail native Ids",
                              e);
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          } finally {
              writableDb.endTransaction();
          }
      }
  
      /**
       * Fetches the preferred detail to use in the contact summary when the name
       * or status field are blank.
       * 
       * @param localContactID The local contact ID
       * @param keyVal The key to fetch (normally either VCARD_PHONE or
       *            VCARD_EMAIL).
       * @param altDetail A contact detail object where the result will be stored.
       * @param readableDb A readable SQLite database object
       * @return true if successful, false otherwise.
       */
      public static boolean fetchPreferredDetail(long localContactID, int keyVal,
              ContactDetail altDetail, SQLiteDatabase readableDb) {
          final int QUERY_COLUMN_LOCALDETAILID = 0;
          final int QUERY_COLUMN_TYPE = 1;
          final int QUERY_COLUMN_VAL = 2;
          final int QUERY_COLUMN_ORDER = 3;
          DatabaseHelper.trace(false, "ContactDetailsTable.fetchPreferredDetail()");
          Cursor c = null;
          try {
              boolean found = false;
              c = readableDb.rawQuery("SELECT " + Field.DETAILLOCALID + "," + Field.TYPE + ","
                      + Field.STRINGVAL + "," + Field.ORDER + " FROM " + TABLE_NAME + " WHERE "
                      + Field.LOCALCONTACTID + "=" + localContactID + " AND " + Field.KEY + "="
                      + keyVal + " AND " + Field.ORDER + "=" + "(SELECT MIN(" + Field.ORDER
                      + ") FROM " + TABLE_NAME + " WHERE " + Field.LOCALCONTACTID + "="
                      + localContactID + " AND " + Field.KEY + "=" + keyVal + ")", null);
              if (c.moveToFirst()) {
                  if (!c.isNull(QUERY_COLUMN_LOCALDETAILID)) {
                      altDetail.localDetailID = c.getLong(QUERY_COLUMN_LOCALDETAILID);
                  }
                  if (!c.isNull(QUERY_COLUMN_TYPE)) {
                      altDetail.keyType = ContactDetail.DetailKeyTypes.values()[c
                              .getInt(QUERY_COLUMN_TYPE)];
                  }
                  if (!c.isNull(QUERY_COLUMN_VAL)) {
                      altDetail.value = c.getString(QUERY_COLUMN_VAL);
                  }
                  if (!c.isNull(QUERY_COLUMN_ORDER)) {
                      altDetail.order = c.getInt(QUERY_COLUMN_ORDER);
                  }
                  found = true;
              }
              return found;
          } catch (SQLException e) {
              LogUtils.logE("ContactDetailsTable.fetchPreferredDetail() SQLException "
                      + "- Unable to fetch preferred detail", e);
              return false;
          } finally {
              CloseUtils.close(c);
              c = null;
          }
      }
  
      /**
       * Fixes the phone numbers and emails of a contact to ensure that at least
       * one of each is a preferred detail.
       * 
       * @param localContactId The local Id of the contact
       * @param writableDb A writable SQLite database object
       * @return SUCCESS or a suitable error code.
       */
      public static ServiceStatus fixPreferredValues(long localContactId, SQLiteDatabase writableDb) {
          DatabaseHelper.trace(false, "ContactDetailsTable.fixPreferredValues()");
          ServiceStatus status = fixPreferredDetail(localContactId,
                  ContactDetail.DetailKeys.VCARD_PHONE, writableDb);
          if (ServiceStatus.SUCCESS != status) {
              return status;
          }
          return fixPreferredDetail(localContactId, ContactDetail.DetailKeys.VCARD_EMAIL, writableDb);
      }
  
      /**
       * Ensures that for a given key there is at least one preferred detail.
       * Modifying the database if necessary.
       * 
       * @param localContactId The local ID of the contact.
       * @param key The key to fix.
       * @param writableDb A writable SQLite database object
       * @return SUCCESS or a suitable error code.
       */
      private static ServiceStatus fixPreferredDetail(long localContactId,
              ContactDetail.DetailKeys key, SQLiteDatabase writableDb) {
          DatabaseHelper.trace(false, "ContactDetailsTable.fixPreferredDetail()");
          ContactDetail altDetail = new ContactDetail();
          if (fetchPreferredDetail(localContactId, key.ordinal(), altDetail, writableDb)) {
              if (altDetail.order > 0) {
                  altDetail = fetchDetail(altDetail.localDetailID, writableDb);
                  if (altDetail != null) {
                      altDetail.order = 0;
                      boolean syncWithServer = (null != altDetail.serverContactId)
                              && (altDetail.serverContactId > -1);
                      boolean syncWithNative = (null != altDetail.syncNativeContactId)
                              && (altDetail.syncNativeContactId > -1);
  
                      return modifyDetail(altDetail, syncWithServer, syncWithNative, writableDb);
                  }
                  return ServiceStatus.ERROR_NOT_FOUND;
              }
          }
          return ServiceStatus.SUCCESS;
      }
  
      /**
       * Returns a cursor of all the contact details that match a specific key and
       * value. Used by the {@link #findNativeContact(Contact, SQLiteDatabase)}
       * method to find all the contacts by name, phone number or email.
       * 
       * @param value The string value to match.
       * @param key The key to match.
       * @param readableDb A readable SQLite database object
       * @return A cursor containing the local detail ID and local contact ID for
       *         each result.
       */
      private static Cursor findDetailByKey(String value, ContactDetail.DetailKeys key,
              SQLiteDatabase readableDb) {
          try {
              if (value == null || key == null) {
                  return null;
              }
              final String searchValue = DatabaseUtils.sqlEscapeString(value);
              return readableDb.rawQuery("SELECT " + Field.DETAILLOCALID + "," + Field.LOCALCONTACTID
                      + " FROM " + TABLE_NAME + " WHERE " + Field.KEY + "=" + key.ordinal() + " AND "
                      + Field.STRINGVAL + "=" + searchValue + " AND " + Field.NATIVECONTACTID
                      + " IS NULL", null);
          } catch (SQLException e) {
              LogUtils.logE("ContactDetailsTable.findDetailByKey() SQLException - "
                      + "Unable to search for native contact ", e);
              return null;
          }
  
      }
      
      /**
       * Moves native details ownership from the duplicate contact to the original
       * contact.
       * 
       * @param info the info for duplicated and original contacts
       * @param nativeInfoList the list of native details from the duplicated
       *            contact
       * @param writableDb A writable SQLite database object
       * @return SUCCESS or a suitable error code.
       */
      public static ServiceStatus mergeContactDetails(ContactIdInfo info, List<ContactDetail> nativeInfoList, SQLiteDatabase writableDb) {
  
          DatabaseHelper.trace(true, "ContactDetailsTable.mergeContactDetails()");
  
          /**
           * Used to hold some contact details info.
           */
          final class DetailsInfo {
  
              /**
               * The detail local id.
               */
              public Long localId;
  
              /**
               * The detail server id.
               */
              public Long serverId;
  
              public DetailsInfo(Long localId, Long serverId) {
                  this.localId = localId;
                  this.serverId = serverId;
              }
          }
  
          // the list of details from the original contact
          List<DetailsInfo> detailLocalIds = new ArrayList<DetailsInfo>();
          // the result cursor from the original contact details query
          Cursor cursor = null;
  
          try {
              // Retrieve a list of detail local IDs from the merged contact
              // (original one)
              final String[] args = {String.valueOf(info.mergedLocalId)}; 
              cursor = writableDb.rawQuery(QUERY_DETAIL_LOCAL_AND_SERVER_IDS_BY_LOCAL_CONTACT_ID, args);
              
              while (cursor.moveToNext()) {
                  if (!cursor.isNull(0) && !cursor.isNull(1)) {
                      // only adding details with a detailServerId (Name and
                      // Nickname don't have detailServerIds)
                      detailLocalIds.add(new DetailsInfo(cursor.getLong(0),cursor
                              .getLong(1)));
                  }
              }
              DatabaseHelper.trace(true, "ContactDetailsTable.mergeContactDetails(): detailLocalIds.size()=" + detailLocalIds.size());
          } 
          catch (Exception e) {
              LogUtils.logE("ContactDetailsTable.mergeContactDetails() Exception - " + "Unable to query merged contact details list", e);
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          } 
          finally {
              if (cursor != null) {
                  cursor.close();
                  cursor = null;
              }
          }
  
          try {
              final ContentValues cv = new ContentValues();
              // Go through the duplicated contact details and find details that
              // matches with the original contact
              // If there is a match, move the native details ownership from the
              // duplicated contact to the original contact
              for (int infoListIndex = 0; infoListIndex < nativeInfoList.size(); infoListIndex++) {
                  final ContactDetail detailInfo = nativeInfoList.get(infoListIndex);
  
                  // Change the ownership
                  for (int detailsIndex = 0; detailsIndex < detailLocalIds.size(); detailsIndex++) {
                      final DetailsInfo currentDetails = detailLocalIds.get(detailsIndex);
                      if (currentDetails.serverId.equals(detailInfo.unique_id)) {
                          cv.put(Field.LOCALCONTACTID.toString(), info.mergedLocalId);
                          cv.put(Field.NATIVECONTACTID.toString(), detailInfo.nativeContactId);
                          cv.put(Field.NATIVEDETAILID.toString(), detailInfo.nativeDetailId);
                          cv.put(Field.NATIVEDETAILVAL1.toString(), detailInfo.nativeVal1);
                          cv.put(Field.NATIVEDETAILVAL2.toString(), detailInfo.nativeVal2);
                          cv.put(Field.NATIVEDETAILVAL3.toString(), detailInfo.nativeVal3);
                          cv.put(Field.NATIVESYNCCONTACTID.toString(), detailInfo.syncNativeContactId);
                          
                          DatabaseHelper.trace(true, "ContactDetailsTable.mergeContactDetails():"
                                  + " changing ownership for duplicated detail: " + detailInfo);
                          writableDb.update(TABLE_NAME, cv, Field.DETAILLOCALID + "=" + currentDetails.localId, null);
                          cv.clear();
                          detailLocalIds.remove(detailsIndex);
                          break;
                      }
                  }
              }
  
              return ServiceStatus.SUCCESS;
          } 
          catch (SQLException e) {
              LogUtils.logE("ContactDetailsTable.mergeContactDetails() SQLException - "
                      + "Unable to merge contact detail native info", e);
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          }
      }
      
      /**
       * Fetches a list of contact details with only the native sync information
       * filled in. This method is used in preference to
       * {@link #fetchContactDetails(Long, List, SQLiteDatabase)} during contact
       * sync operations to improve performance.
       * 
       * @param localContactId Local Id of the contact (in the database).
       * @param nativeInfoList A list which will be filled with contact detail
       *            objects.
       * @param writableDb A writable SQLite database object
       * @return SUCCESS or a suitable error code.
       */
      public static ServiceStatus fetchNativeInfo(long localContactId,
              List<ContactDetail> nativeInfoList, SQLiteDatabase writableDb) {
          DatabaseHelper.trace(true, "ContactDetailsTable.fetchNativeInfo()");
          Cursor c = null;
          try {
              final String[] args = {
                  String.valueOf(localContactId)
              };
              c = writableDb.rawQuery(QUERY_DETAIL_BY_LOCAL_CONTACT_ID, args);
              nativeInfoList.clear();
              while (c.moveToNext()) {
                  ContactDetail detailInfo = new ContactDetail();
                  if (!c.isNull(QUERY_COLUMN_LOCALDETAILID)) {
                      detailInfo.localDetailID = c.getLong(QUERY_COLUMN_LOCALDETAILID);
                  }
                  if (!c.isNull(QUERY_COLUMN_KEY)) {
                      detailInfo.key = ContactDetail.DetailKeys.values()[c.getInt(QUERY_COLUMN_KEY)];
                  }
                  if (!c.isNull(QUERY_COLUMN_SERVERDETAILID)) {
                      detailInfo.unique_id = c.getLong(QUERY_COLUMN_SERVERDETAILID);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVECONTACTID)) {
                      detailInfo.nativeContactId = c.getInt(QUERY_COLUMN_NATIVECONTACTID);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVEDETAILID)) {
                      detailInfo.nativeDetailId = c.getInt(QUERY_COLUMN_NATIVEDETAILID);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVEVAL1)) {
                      detailInfo.nativeVal1 = c.getString(QUERY_COLUMN_NATIVEVAL1);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVEVAL2)) {
                      detailInfo.nativeVal2 = c.getString(QUERY_COLUMN_NATIVEVAL2);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVEVAL3)) {
                      detailInfo.nativeVal3 = c.getString(QUERY_COLUMN_NATIVEVAL3);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVESYNCCONTACTID)) {
                      detailInfo.syncNativeContactId = c.getInt(QUERY_COLUMN_NATIVESYNCCONTACTID);
                  }
                  nativeInfoList.add(detailInfo);
              }
          } catch (SQLException e) {
              LogUtils.logE("ContactDetailsTable.fetchNativeInfo() - error:\n", e);
              return ServiceStatus.ERROR_DATABASE_CORRUPT;
          } finally {
              CloseUtils.close(c);
              c = null;
          }
          return ServiceStatus.SUCCESS;
      }
      
  
      /**
       * This method finds the localContactId corresponding to the wanted Key and
       * StringVal fields.
       * 
       * @param value - StringVal
       * @param key - Key
       * @param readableDb A readable SQLite database object
       * @return - localContactId (it is unique)
       */
      public static long findLocalContactIdByKey(String networkName, String value,
              ContactDetail.DetailKeys key, SQLiteDatabase readableDb) throws SQLException {
          long localContactId = -1;
          Cursor c = null;
          try {
              if (value == null || key == null) {
                  return localContactId;
              }
              value = DatabaseUtils.sqlEscapeString(value);
              networkName = DatabaseUtils.sqlEscapeString(networkName);
  
              StringBuffer query = StringBufferPool.getStringBuffer(SQLKeys.SELECT);
              query.append(Field.LOCALCONTACTID).append(SQLKeys.FROM).append(TABLE_NAME).append(
                      SQLKeys.WHERE).append(Field.KEY).append(SQLKeys.EQUALS).append(key.ordinal())
                      .append(SQLKeys.AND).append(Field.STRINGVAL).append(SQLKeys.EQUALS).append(
                              value);
  
              if (!TextUtils.isEmpty(networkName)) {
                  query.append(SQLKeys.AND).append(Field.ALT).append(SQLKeys.EQUALS).append(
                          networkName);
              }
              c = readableDb.rawQuery(StringBufferPool.toStringThenRelease(query), null);
              while (c.moveToNext()) {
                  if (!c.isNull(0)) {
                      localContactId = c.getLong(0);
                      break;
                  }
              }
          } finally {
              CloseUtils.close(c);
              c = null;
          }
          return localContactId;
      }
  
      /**
       * This method finds the chat id corresponding to the wanted Alt,
       * LocalContactId and Key fields
       * 
       * @param networkName - network name (google,MSN)
       * @param localContactId - localContactId of the contcat
       * @param readableDb A readable SQLite database object
       * @return - chatId for this network(it is unique)
       */
      public static String findChatIdByLocalContactIdAndNetwork(String networkName,
              long localContactId, SQLiteDatabase readableDb) throws NullPointerException,
              SQLException {
          if (readableDb == null) {
              throw new NullPointerException(
                      "ContactDetailsTable.findChatIdByLocalContactIdAndNetwork(): The database passed in was null!");
          }
          String chatId = null;
          Cursor c = null;
          try {
              if (localContactId == -1 || networkName == null) {
                  return chatId;
              }
              networkName = DatabaseUtils.sqlEscapeString(networkName);
              String query = "SELECT "
                      + Field.STRINGVAL
                      + " FROM "
                      + TABLE_NAME
                      + " WHERE "
                      + Field.KEY
                      + "="
                      + ContactDetail.DetailKeys.VCARD_IMADDRESS.ordinal()
                      + " AND "
                      + Field.LOCALCONTACTID
                      + "="
                      + (!TextUtils.isEmpty(networkName) ? localContactId + " AND "
                              + Field.ALT + "=" + networkName : String.valueOf(localContactId));
              c = readableDb.rawQuery(query, null);
              while (c.moveToNext()) {
                  if (!c.isNull(0)) {
                      chatId = c.getString(0);
                      break;
                  }
              }
          } finally {
              CloseUtils.close(c);
          }
          return chatId;
      }
  
      /**
       * Compares the details given with a specific contact in the database. If
       * the contacts match a list of native sync information is returned.
       * 
       * @param c Contact to compare
       * @param localContactId Contact in the database to compare with
       * @param detailIdList A list which will be populated if a match is found.
       * @param readableDb A readable SQLite database object
       * @return true if a match is found, false otherwise
       */
      public static boolean doContactsMatch(Contact c, long localContactId,
              List<NativeIdInfo> detailIdList, SQLiteDatabase readableDb) {
          List<ContactDetail> srcDetails = new ArrayList<ContactDetail>();
          ServiceStatus status = fetchContactDetails(localContactId, srcDetails, readableDb);
          if (ServiceStatus.SUCCESS != status) {
              return false;
          }
          detailIdList.clear();
          for (int i = 0; i < c.details.size(); i++) {
              final ContactDetail destDetail = c.details.get(i);
              Long foundDetailId = null;
              for (int j = 0; j < srcDetails.size(); j++) {
                  final ContactDetail srcDetail = srcDetails.get(j);
                  if (srcDetail.changeID == null && srcDetail.key != null
                          && srcDetail.key.equals(destDetail.key)) {
                      if (srcDetail.value != null && srcDetail.value.equals(destDetail.value)) {
                          foundDetailId = srcDetail.localDetailID;
                          srcDetail.changeID = 1L;
                          break;
                      }
                      if (srcDetail.value == null && destDetail.value == null) {
                          foundDetailId = srcDetail.localDetailID;
                          srcDetail.changeID = 1L;
                          break;
                      }
                      if (srcDetail.key == ContactDetail.DetailKeys.VCARD_NAME
                              && srcDetail.value != null
                              && srcDetail.value.indexOf(VCardHelper.LIST_SEPARATOR) < 0) {
                          VCardHelper.Name name1 = srcDetail.getName();
                          VCardHelper.Name name2 = destDetail.getName();
                          if (name1 != null && name2 != null
                                  && name1.toString().equals(name2.toString())) {
                              foundDetailId = srcDetail.localDetailID;
                              srcDetail.changeID = 1L;
                          }
                      }
                      if (srcDetail.key == ContactDetail.DetailKeys.VCARD_ADDRESS
                              && srcDetail.value != null
                              && srcDetail.value.indexOf(VCardHelper.LIST_SEPARATOR) < 0) {
                          VCardHelper.PostalAddress addr1 = srcDetail.getPostalAddress();
                          VCardHelper.PostalAddress addr2 = destDetail.getPostalAddress();
                          if (addr1 != null && addr2 != null
                                  && addr1.toString().equals(addr2.toString())) {
                              foundDetailId = srcDetail.localDetailID;
                              srcDetail.changeID = 1L;
                          }
                      }
                  }
              }
              if (foundDetailId == null) {
                  if (destDetail.value != null && destDetail.value.length() > 0) {
                      LogUtils.logD("ContactDetailTable.doContactsMatch - The detail "
                              + destDetail.key + ", <" + destDetail.value + "> was not found");
                      for (int j = 0; j < srcDetails.size(); j++) {
                          final ContactDetail srcDetail = srcDetails.get(j);
                          if (srcDetail.key != null && srcDetail.key.equals(destDetail.key)) {
                              LogUtils.logD("ContactDetailTable.doContactsMatch - No Match Key: "
                                      + srcDetail.key + ", Value: <" + srcDetail.value + ">, Used: "
                                      + srcDetail.changeID);
                          }
                      }
                      return false;
                  }
              } else {
                  NativeIdInfo nativeIdInfo = new NativeIdInfo();
                  nativeIdInfo.localId = foundDetailId;
                  nativeIdInfo.nativeContactId = c.nativeContactId;
                  nativeIdInfo.nativeDetailId = destDetail.nativeDetailId;
                  nativeIdInfo.nativeVal1 = destDetail.nativeVal1;
                  nativeIdInfo.nativeVal2 = destDetail.nativeVal2;
                  nativeIdInfo.nativeVal3 = destDetail.nativeVal3;
                  detailIdList.add(nativeIdInfo);
              }
          }
          for (int j = 0; j < srcDetails.size(); j++) {
              final ContactDetail srcDetail = srcDetails.get(j);
              if (srcDetail.nativeContactId == null) {
                  boolean found = false;
                  for (int i = 0; i < detailIdList.size(); i++) {
                      NativeIdInfo info = detailIdList.get(i);
                      if (info.localId == srcDetail.localDetailID.longValue()) {
                          found = true;
                          break;
                      }
                  }
                  if (!found) {
                      NativeIdInfo nativeIdInfo = new NativeIdInfo();
                      nativeIdInfo.localId = srcDetail.localDetailID;
                      nativeIdInfo.nativeContactId = srcDetail.nativeContactId;
                      nativeIdInfo.nativeDetailId = srcDetail.nativeDetailId;
                      nativeIdInfo.nativeVal1 = srcDetail.nativeVal1;
                      nativeIdInfo.nativeVal2 = srcDetail.nativeVal2;
                      nativeIdInfo.nativeVal3 = srcDetail.nativeVal3;
                      nativeIdInfo.syncNativeContactId = c.nativeContactId;
                      detailIdList.add(nativeIdInfo);
                  }
              }
          }
          return true;
      }
  
      /**
       * Searches the contact details table for a contact from the native
       * phonebook. If a match is found, the native sync information is transfered
       * from the given contact into the matching database contact. Tries to match
       * in the following sequence: 1) If there is a name, match by name 2)
       * Otherwise, if there is a phone number, match by number 3) Otherwise, if
       * there is an email, match by email 4) Otherwise return false For a match
       * to occur, all given contact details must be identical to those in the
       * database. There may be more details in the database but this won't change
       * the result.
       * 
       * @param c The contact to match
       * @param writableDb A writable SQLite database object
       * @return true if the contact was found, false otherwise
       */
      public static boolean findNativeContact(Contact c, SQLiteDatabase writableDb) {
          String name = null;
          String phone = null;
          String email = null;
          List<ContactIdInfo> contactIdList = new ArrayList<ContactIdInfo>();
          List<NativeIdInfo> detailIdList = new ArrayList<NativeIdInfo>();
          for (int i = 0; i < c.details.size(); i++) {
              if (c.details.get(i).key == ContactDetail.DetailKeys.VCARD_NICKNAME) {
                  name = c.details.get(i).getValue();
                  if (name != null && name.length() > 0) {
                      break;
                  }
              }
              if (c.details.get(i).key == ContactDetail.DetailKeys.VCARD_PHONE) {
                  if (phone == null || phone.length() > 0) {
                      phone = c.details.get(i).getValue();
                  }
              }
              if (c.details.get(i).key == ContactDetail.DetailKeys.VCARD_EMAIL) {
                  if (email == null || email.length() > 0) {
                      email = c.details.get(i).getValue();
                  }
              }
          }
          Cursor candidateListCursor = null;
          if (name != null && name.length() > 0) {
              LogUtils.logD("ContactDetailsTable.findNativeContact - "
                      + "Searching for contact called " + name);
              candidateListCursor = findDetailByKey(name, ContactDetail.DetailKeys.VCARD_NICKNAME,
                      writableDb);
          } else if (phone != null && phone.length() > 0) {
              LogUtils.logD("ContactDetailsTable.findNativeContact - "
                      + "Searching for contact with phone " + phone);
              candidateListCursor = findDetailByKey(phone, ContactDetail.DetailKeys.VCARD_PHONE,
                      writableDb);
          } else if (email != null && email.length() > 0) {
              LogUtils.logD("ContactDetailsTable.findNativeContact - "
                      + "Searching for contact with email " + email);
              candidateListCursor = findDetailByKey(email, ContactDetail.DetailKeys.VCARD_EMAIL,
                      writableDb);
          }
          List<NativeIdInfo> tempDetailIdList = new ArrayList<NativeIdInfo>();
          List<NativeIdInfo> currentDetailIdList = new ArrayList<NativeIdInfo>();
          Integer minNoOfDetails = null;
          Long chosenContactId = null;
          if (candidateListCursor != null) {
              while (candidateListCursor.moveToNext()) {
                  long localContactId = candidateListCursor.getLong(1);
                  tempDetailIdList.clear();
                  if (doContactsMatch(c, localContactId, tempDetailIdList, writableDb)) {
                      if (minNoOfDetails == null
                              || minNoOfDetails.intValue() > tempDetailIdList.size()) {
                          if (ContactsTable.fetchSyncToPhone(localContactId, writableDb)) {
                              minNoOfDetails = tempDetailIdList.size();
                              chosenContactId = localContactId;
                              currentDetailIdList.clear();
                              currentDetailIdList.addAll(tempDetailIdList);
                          }
                      }
                  }
              }
              candidateListCursor.close();
              if (chosenContactId != null) {
                  LogUtils.logD("ContactDetailsTable.findNativeContact - "
                          + "Found contact (no need to add)");
                  ContactIdInfo contactIdInfo = new ContactIdInfo();
                  contactIdInfo.localId = chosenContactId;
                  contactIdInfo.nativeId = c.nativeContactId;
                  contactIdList.add(contactIdInfo);
                  detailIdList.addAll(currentDetailIdList);
                  // Update contact IDs of the contacts which are already in the
                  // database
                  ServiceStatus status = ContactsTable.syncSetNativeIds(contactIdList, writableDb);
                  if (ServiceStatus.SUCCESS != status) {
                      return false;
                  }
                  status = ContactSummaryTable.syncSetNativeIds(contactIdList, writableDb);
                  if (ServiceStatus.SUCCESS != status) {
                      return false;
                  }
                  status = ContactDetailsTable.syncSetNativeIds(detailIdList, writableDb);
                  if (ServiceStatus.SUCCESS != status) {
                      return false;
                  }
                  return true;
              }
          }
          LogUtils.logD("ContactDetailsTable.findNativeContact - Contact not found (will be added)");
          return false;
      }
  
      /**
       * Fetches all the details that have changed and need to be synced with the
       * server. Details associated with new contacts are returned separately,
       * this is determined by a parameter. The
       * {@link #syncSetServerIds(List, SQLiteDatabase)} method is used to mark
       * the details as up to date, once the sync has completed.
       * 
       * @param readableDb A readable SQLite database object
       * @param newContacts true if details associated with new contacts should be
       *            returned, otherwise modified details are returned.
       * @return A cursor which can be passed into the
       *         {@link #syncServerGetNextNewContactDetails(Cursor, List, int)}
       *         method.
       */
      public static Cursor syncServerFetchContactChanges(SQLiteDatabase readableDb,
              boolean newContacts) {
          try {
              String statusMatch = "";
              if (newContacts) {
                  statusMatch = Field.SERVERSYNCCONTACTID + " IS NULL";
              } else {
                  /** Note this won't return NULLs. **/
                  statusMatch = Field.SERVERSYNCCONTACTID + "<>-1";
              }
              return readableDb.rawQuery("SELECT " + Field.LOCALCONTACTID + ","
                      + Field.SERVERSYNCCONTACTID + "," + Field.DETAILLOCALID + ","
                      + Field.DETAILSERVERID + "," + Field.KEY + "," + Field.TYPE + ","
                      + Field.STRINGVAL + "," + Field.ORDER + "," + Field.PHOTOURL + " FROM "
                      + TABLE_NAME + " WHERE " + statusMatch, null);
          } catch (SQLException e) {
              LogUtils.logE("ContactDetailsTable.syncServerFetchContactChanges() SQLException - "
                      + "Unable to search for native contact ", e);
              return null;
          }
      }
  
      /**
       * Returns the next batch of contacts which need to be added on the server.
       * The {@link #syncServerFetchContactChanges(SQLiteDatabase, boolean)}
       * method is used to retrieve the cursor initially, then this function can
       * be called many times until all the contacts have been fetched. When the
       * list returned from this method is empty the cursor has reached the end
       * and the sync is complete.
       * 
       * @param c The cursor (see description above)
       * @param contactList Will be filled with contacts that need to be added to
       *            the server
       * @param maxContactsToFetch Maximum number of contacts to return in the
       *            list. The function can be called in a loop until all the
       *            contacts have been retrieved.
       */
      public static void syncServerGetNextNewContactDetails(Cursor c, List<Contact> contactList,
              int maxContactsToFetch) {
          final int QUERY_COLUMN_LOCALCONTACTID = 0;
          final int QUERY_COLUMN_SERVERSYNCCONTACTID = 1;
          final int QUERY_COLUMN_LOCALDETAILID = 2;
          final int QUERY_COLUMN_SERVERDETAILID = 3;
          final int QUERY_COLUMN_KEY = 4;
          final int QUERY_COLUMN_KEYTYPE = 5;
          final int QUERY_COLUMN_VAL = 6;
          final int QUERY_COLUMN_ORDER = 7;
          final int QUERY_COLUMN_PHOTOURL = 8;
          contactList.clear();
          Contact currentContact = null;
          while (c.moveToNext()) {
              final ContactDetail detail = new ContactDetail();
              if (!c.isNull(QUERY_COLUMN_LOCALCONTACTID)) {
                  detail.localContactID = c.getLong(QUERY_COLUMN_LOCALCONTACTID);
              }
              if (!c.isNull(QUERY_COLUMN_SERVERSYNCCONTACTID)) {
                  detail.serverContactId = c.getLong(QUERY_COLUMN_SERVERSYNCCONTACTID);
              }
              if (currentContact == null
                      || !currentContact.localContactID.equals(detail.localContactID)) {
                  if (contactList.size() >= maxContactsToFetch) {
                      if (currentContact != null) {
                          c.moveToPrevious();
                      }
                      break;
                  }
                  currentContact = new Contact();
                  currentContact.localContactID = detail.localContactID;
                  if (detail.serverContactId == null) {
                      currentContact.synctophone = true;
                  }
                  currentContact.contactID = detail.serverContactId;
                  contactList.add(currentContact);
              }
              if (!c.isNull(QUERY_COLUMN_LOCALDETAILID)) {
                  detail.localDetailID = c.getLong(QUERY_COLUMN_LOCALDETAILID);
              }
              if (!c.isNull(QUERY_COLUMN_SERVERDETAILID)) {
                  detail.unique_id = c.getLong(QUERY_COLUMN_SERVERDETAILID);
              }
              detail.key = ContactDetail.DetailKeys.values()[c.getInt(QUERY_COLUMN_KEY)];
              if (!c.isNull(QUERY_COLUMN_KEYTYPE)) {
                  detail.keyType = ContactDetail.DetailKeyTypes.values()[c
                          .getInt(QUERY_COLUMN_KEYTYPE)];
              }
              detail.value = c.getString(QUERY_COLUMN_VAL);
              if (!c.isNull(QUERY_COLUMN_ORDER)) {
                  detail.order = c.getInt(QUERY_COLUMN_ORDER);
              }
              if (!c.isNull(QUERY_COLUMN_PHOTOURL)) {
                  detail.photo_url = c.getString(QUERY_COLUMN_PHOTOURL);
              }
              currentContact.details.add(detail);
          }
      }
  
      /**
       * Retrieves the total number of details that have changed and need to be
       * synced with the server. Includes both new and modified contacts.
       * 
       * @param db Readable SQLiteDatabase object.
       * @return The number of details.
       */
      public static int syncServerFetchNoOfChanges(final SQLiteDatabase db) {
          if (Settings.ENABLED_DATABASE_TRACE) {
              DatabaseHelper.trace(false, "ContactDetailsTable." + "syncServerFetchNoOfChanges()");
          }
  
          Cursor cursor = null;
          try {
              /** Return all values from this table (including new contacts) **/
              cursor = db.rawQuery("SELECT COUNT(distinct " + Field.LOCALCONTACTID + ") FROM "
                      + TABLE_NAME + " WHERE " + Field.SERVERSYNCCONTACTID + "<>-1 OR "
                      + Field.SERVERSYNCCONTACTID + " IS NULL", null);
              if (cursor.moveToFirst()) {
                  int result = cursor.getInt(0);
                  return result;
              } else {
                  LogUtils.logE("ContactDetailsTable." + "syncServerFetchNoOfChanges() COUNT(*) "
                          + "should not return an empty cursor, returning 0");
                  return 0;
              }
  
          } finally {
              CloseUtils.close(cursor);
          }
      }
  
      /**
       * Fetches all the details that have changed and need to be synced with the
       * native. Details associated with new contacts are returned separately,
       * this is determined by a parameter. The
       * {@link #syncSetNativeIds(List, SQLiteDatabase)} method is used to mark
       * the details as up to date, once the sync has completed.
       * 
       * @param readableDb A readable SQLite database object
       * @param newContacts true if details associated with new contacts should be
       *            returned, otherwise modified details are returned.
       * @return A cursor which can be passed into the
       *         {@link #syncNativeGetNextNewContactDetails(Cursor, List, int)}
       *         method.
       */
      public static Cursor syncNativeFetchContactChanges(SQLiteDatabase readableDb,
              boolean newContacts) {
          try {
              String statusMatch = "";
              if (newContacts) {
                  statusMatch = Field.NATIVESYNCCONTACTID + " IS NULL";
              } else {
                  /** Note this won't return NULLs. **/
                  statusMatch = Field.NATIVESYNCCONTACTID + "<>-1";
              }
              return readableDb.rawQuery("SELECT " + Field.LOCALCONTACTID + ","
                      + Field.NATIVECONTACTID + "," + Field.NATIVESYNCCONTACTID + ","
                      + Field.DETAILLOCALID + "," + Field.NATIVEDETAILID + "," + Field.KEY + ","
                      + Field.TYPE + "," + Field.STRINGVAL + "," + Field.ORDER + ","
                      + Field.NATIVEDETAILVAL1 + "," + Field.NATIVEDETAILVAL2 + ","
                      + Field.NATIVEDETAILVAL3 + " FROM " + TABLE_NAME + " WHERE " + statusMatch
                      + " ORDER BY " + Field.LOCALCONTACTID, null);
          } catch (SQLException e) {
              LogUtils.logE("ContactDetailsTable.findDetailByKey() SQLException - "
                      + "Unable to search for native contact ", e);
              return null;
          }
      }
  
      /**
       * Returns the next batch of contacts which need to be added on the native
       * database. The
       * {@link #syncNativeFetchContactChanges(SQLiteDatabase, boolean)} method is
       * used to retrieve the cursor initially, then this function can be called
       * many times until all the contacts have been fetched. When the list
       * returned from this method is empty the cursor has reached the end and the
       * sync is complete.
       * 
       * @param c The cursor (see description above)
       * @param contactList Will be filled with contacts that need to be added to
       *            the native
       * @param maxContactsToFetch Maximum number of contacts to return in the
       *            list. The function can be called in a loop until all the
       *            contacts have been retrieved.
       * @return true if successful, false if an error occurred (there seems to be
       *         a defect in Android which occasionally causes this to fail due to
       *         bad cursor state. The workaround is to retry the operation).
       */
      public static boolean syncNativeGetNextNewContactDetails(Cursor c, List<Contact> contactList,
              int maxContactsToFetch) {
          final int QUERY_COLUMN_LOCALCONTACTID = 0;
          final int QUERY_COLUMN_NATIVECONTACTID = 1;
          final int QUERY_COLUMN_NATIVESYNCCONTACTID = 2;
          final int QUERY_COLUMN_LOCALDETAILID = 3;
          final int QUERY_COLUMN_NATIVEDETAILID = 4;
          final int QUERY_COLUMN_KEY = 5;
          final int QUERY_COLUMN_KEYTYPE = 6;
          final int QUERY_COLUMN_VAL = 7;
          final int QUERY_COLUMN_ORDER = 8;
          final int QUERY_COLUMN_NATIVEVAL1 = 9;
          final int QUERY_COLUMN_NATIVEVAL2 = 10;
          final int QUERY_COLUMN_NATIVEVAL3 = 11;
          try {
              contactList.clear();
              Contact currentContact = null;
              while (c.moveToNext()) {
                  final ContactDetail detail = new ContactDetail();
                  detail.localContactID = c.getLong(QUERY_COLUMN_LOCALCONTACTID);
                  if (!c.isNull(QUERY_COLUMN_NATIVECONTACTID)) {
                      detail.nativeContactId = c.getInt(QUERY_COLUMN_NATIVECONTACTID);
                  } else {
                      detail.nativeContactId = null;
                  }
                  if (currentContact == null
                          || !currentContact.localContactID.equals(detail.localContactID)) {
                      if (contactList.size() >= maxContactsToFetch) {
                          if (currentContact != null) {
                              c.moveToPrevious();
                          }
                          break;
                      }
                      currentContact = new Contact();
                      currentContact.localContactID = detail.localContactID;
                      currentContact.nativeContactId = detail.nativeContactId;
                      contactList.add(currentContact);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVESYNCCONTACTID)) {
                      detail.syncNativeContactId = c.getInt(QUERY_COLUMN_NATIVESYNCCONTACTID);
                  } else {
                      detail.syncNativeContactId = null;
                  }
                  if (!c.isNull(QUERY_COLUMN_LOCALDETAILID)) {
                      detail.localDetailID = c.getLong(QUERY_COLUMN_LOCALDETAILID);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVEDETAILID)) {
                      detail.nativeDetailId = c.getInt(QUERY_COLUMN_NATIVEDETAILID);
                  } else {
                      detail.nativeDetailId = null;
                  }
                  detail.key = ContactDetail.DetailKeys.values()[c.getInt(QUERY_COLUMN_KEY)];
                  if (!c.isNull(QUERY_COLUMN_KEYTYPE)) {
                      detail.keyType = ContactDetail.DetailKeyTypes.values()[c
                              .getInt(QUERY_COLUMN_KEYTYPE)];
                  }
                  detail.value = c.getString(QUERY_COLUMN_VAL);
                  if (!c.isNull(QUERY_COLUMN_ORDER)) {
                      detail.order = c.getInt(QUERY_COLUMN_ORDER);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVEVAL1)) {
                      detail.nativeVal1 = c.getString(QUERY_COLUMN_NATIVEVAL1);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVEVAL2)) {
                      detail.nativeVal2 = c.getString(QUERY_COLUMN_NATIVEVAL2);
                  }
                  if (!c.isNull(QUERY_COLUMN_NATIVEVAL3)) {
                      detail.nativeVal3 = c.getString(QUERY_COLUMN_NATIVEVAL3);
                  }
                  currentContact.details.add(detail);
              }
              return true;
          } catch (IllegalStateException e) {
              LogUtils.logE("ContactDetailsTable.syncNativeGetNextNewContactDetails - "
                      + "Unable to fetch modified contacts from people\n" + e);
              c.requery();
              return false;
          }
      }
  
      /**
       * Retrieves the total number of details that have changed and need to be
       * synced with the native database. Does not include new contacts.
       * 
       * @param readableDb A readable SQLite database object
       * @return The number of details.
       */
      public static int syncNativeFetchNoOfChanges(SQLiteDatabase readableDb) throws SQLException {
          if (Settings.ENABLED_DATABASE_TRACE)
              DatabaseHelper.trace(false, "ContactDetailsTable.fetchNoOfContactDetailChanges()");
          int noOfChanges = 0;
          Cursor c = null;
          try {
              c = readableDb.rawQuery("SELECT COUNT(*) FROM " + TABLE_NAME + " WHERE "
                      + Field.NATIVESYNCCONTACTID + "<>-1", null);
              if (c.moveToFirst()) {
                  noOfChanges = c.getInt(0);
              }
          } finally {
              CloseUtils.close(c);
              c = null;
          }
          return noOfChanges;
      }
  
      /**
       * Remove all preferred details from contact for a particular key.
       * 
       * @param localContactID Identifies the contact in the database
       * @param key The contact detail key (phone, email, address, etc.)
       * @param writableDb A writable SQLite database object
       */
      public static boolean removePreferred(Long localContactID, DetailKeys key,
              SQLiteDatabase writableDb) {
          try {
              ContentValues cv = new ContentValues();
              cv.put(Field.ORDER.toString(), ContactDetail.ORDER_NORMAL);
              String[] args = {
                      String.valueOf(localContactID), String.valueOf(key.ordinal())
              };
              writableDb.update(TABLE_NAME, cv, Field.LOCALCONTACTID + "=? AND " + Field.KEY + "=?",
                      args);
              return true;
          } catch (SQLException e) {
              LogUtils.logE("ContactDetailsTable.removePreferred - "
                      + "Unable to clear preferred details, error:\n" + e);
              return false;
          }
      }
      
      /**
       * Maps a ContactChange flag to a ContactDetail key type.
       * 
       * @see ContactChange#FLAG_XXX
       * @see ContactDetail#DetailKeyTypes
       * 
       * @param flag the ContactChange flag to convert
       * @return the ContactDetail key type equivalent
       */
      public static int mapContactChangeFlagToInternalType(int flag) {
         
          // FIXME: We may be losing data at this stage because when the type is
          // a mix of HOME and FAX, it gets converted into FAX...
          
          int internalType = DetailKeyTypes.UNKNOWN.ordinal();
          
          if ((flag & ContactChange.FLAG_HOME) == ContactChange.FLAG_HOME) {
              
              internalType = DetailKeyTypes.HOME.ordinal();
              
          } else if ((flag & ContactChange.FLAG_CELL) == ContactChange.FLAG_CELL) {
              
              internalType = DetailKeyTypes.CELL.ordinal();
              
          } else if ((flag & ContactChange.FLAG_WORK) == ContactChange.FLAG_WORK) {
              
              internalType = DetailKeyTypes.WORK.ordinal();
  
          } else if ((flag & ContactChange.FLAG_FAX) == ContactChange.FLAG_FAX) {
              
              internalType = DetailKeyTypes.FAX.ordinal();
  
          } else if ((flag & ContactChange.FLAG_BIRTHDAY) == ContactChange.FLAG_BIRTHDAY) {
              
              internalType = DetailKeyTypes.BIRTHDAY.ordinal();
  
          }
          
          return internalType;
      }
      
      /**
       * Maps a ContactChange flag to a ContactDetail order.
       * 
       * @see ContactChange#FLAG_XXX
       * @see ContactDetail#ORDER_XXX
       * 
       * @param flag the ContactChange flag to convert
       * @return the ContactDetail order equivalent
       */
      public static int mapContactChangeFlagToInternalOrder(int flag) {
          
          if ((flag & ContactChange.FLAG_PREFERRED) == ContactChange.FLAG_PREFERRED) {
              return ContactDetail.ORDER_PREFERRED;
          }
          
          return ContactDetail.ORDER_NORMAL;
      }
      
      /**
       * Maps a ContactChange key to a ContactDetail key.
       * 
       * @see ContactChange#KEY_XXX
       * @see ContactDetail#DetailKeys
       * 
       * @param key the ContactChange key to convert
       * @return the ContactDetail key equivalent
       */
      public static int mapContactChangeKeyToInternalKey(int key) {
          
          return key == ContactChange.KEY_UNKNOWN ? ContactDetail.DetailKeys.UNKNOWN.ordinal() : key - 1;
      }
      
      /**
       * Maps ContactDetail key to ContactChange key.
       * 
       * @see ContactChange#KEY_XXX
       * @see ContactDetail#DetailKeys
       * 
       * @param key the ContactDetail key to convert
       * @return the ContactChange key equivalent
       */
      public static int mapInternalKeyToContactChangeKey(int key) {
          
          return key == ContactDetail.DetailKeys.UNKNOWN.ordinal() ? ContactChange.KEY_UNKNOWN : key + 1;
      }
      
      /**
       * Maps a ContactDetail type and an order to the ContactChange flag equivalent.
       * 
       * @see ContactChange#FLAG_XXX
       * @see ContactDetail#ORDER_XXX
       * @see ContactDetail#DetailKeyTypes
       * 
       * @param type the ContactDetail type
       * @param order the ContactDetail order
       * @return the ContactChange flag equivalent
       */
      public static int mapInternalTypeAndOrderToContactChangeFlag(int type, int order) {
          
          int flags = ContactChange.FLAG_NONE;
          
          if (order == ContactDetail.ORDER_PREFERRED) {
              
              flags |= ContactChange.FLAG_PREFERRED;
          }
          
          if (type == ContactDetail.DetailKeyTypes.CELL.ordinal()
           || type == ContactDetail.DetailKeyTypes.MOBILE.ordinal()) {
              
              flags |= ContactChange.FLAG_CELL;
          } else if (type == ContactDetail.DetailKeyTypes.HOME.ordinal()) {
              
              flags |= ContactChange.FLAG_HOME;
          } else if (type == ContactDetail.DetailKeyTypes.WORK.ordinal()) {
              
              flags |= ContactChange.FLAG_WORK;
          } else if (type == ContactDetail.DetailKeyTypes.BIRTHDAY.ordinal()) {
              
              flags |= ContactChange.FLAG_BIRTHDAY;
          } else if (type == ContactDetail.DetailKeyTypes.FAX.ordinal()) {
              
              flags |= ContactChange.FLAG_FAX;
          }
  
          return flags;
      }
      
      /**
       * Fills a ContentValues object with the provided ContactChange to later be used for database insert.
       * 
       * @param contactChange the ContactChange to get the values from
       * @param values the ContentValues object to fill
       */
      public static void prepareNativeContactDetailInsert(ContactChange contactChange, ContentValues values) {
          
          final long nativeContactId = contactChange.getNabContactId();
          
          // add the key
          values.put(Field.KEY.toString(), mapContactChangeKeyToInternalKey(contactChange.getKey()));
          
          // add the type
          values.put(Field.TYPE.toString(), mapContactChangeFlagToInternalType(contactChange.getFlags()));
          
          // add the string value
          values.put(Field.STRINGVAL.toString(), contactChange.getValue());
          
          // add the order number
          values.put(Field.ORDER.toString(), mapContactChangeFlagToInternalOrder(contactChange.getFlags()));
          
          // add the local contact id
          values.put(Field.LOCALCONTACTID.toString(), contactChange.getInternalContactId());
          
          // add the native ids if valid
          if (nativeContactId != ContactChange.INVALID_ID) {
          
              // add the native contact id
              values.put(Field.NATIVECONTACTID.toString(), nativeContactId);
              
              // add the native detail id
              values.put(Field.NATIVEDETAILID.toString(), contactChange.getNabDetailId());
              
              // set the NativeSyncContactId to -1 (means no need to sync that row to native)
              values.put(Field.NATIVESYNCCONTACTID.toString(), -1);
          }
      }
      
      /**
       * Adds the provided contact details to the ContactDetail table.
       * 
       * Note: the provided ContactChange are modified with the corresponding internalDetailId from the
       *       database insertion.
       * 
       * @see ContactChange
       * 
       * @param contactChange the contact details
       * @param writeableDb the db where to write
       * @return true if successful, false otherwise
       */
      public static boolean addNativeContactDetails(ContactChange[] contactChange, SQLiteDatabase writeableDb) {
          
          try {
              final ContentValues values = new ContentValues();
              ContactChange currentChange;
              
              // go through the ContactChange and add their details to the ContactDetails table
              for (int i = 0; i < contactChange.length; i++) {
                  
                  currentChange = contactChange[i];
                  values.clear();
                  prepareNativeContactDetailInsert(currentChange, values);
                  currentChange.setInternalDetailId(writeableDb.insertOrThrow(TABLE_NAME, null, values));
              }
          }
          catch(Exception e) {
              
              return false;
          }
          
          return true;
      }
  
      /**
       * Gets an array of ContactChange from the contact's local id.
       * 
       * @see ContactChange
       * 
       * @param localId the local id of the contact to get
       * @return an array of ContactChange
       */
      public static ContactChange[] getContactChanges(long localId, boolean nativeSyncableOnly, SQLiteDatabase readableDb) {
          
          final String[] SELECTION = { String.valueOf(localId) };
          final String QUERY_STRING = nativeSyncableOnly ? QUERY_NATIVE_SYNCABLE_CONTACT_DETAILS_BY_LOCAL_ID : QUERY_CONTACT_DETAILS_BY_LOCAL_ID;
          Cursor cursor = null;
          
          try {
              
              cursor = readableDb.rawQuery(QUERY_STRING , SELECTION );
              
              if (cursor.getCount() > 0) {
                  
                  final ContactChange[] changes = new ContactChange[cursor.getCount()];
                  int index = 0;
                  
                  while (cursor.moveToNext()) {
                      
                      // fill the ContactChange class with contact detail data if not empty
                      // StringVal=7
                      String value = cursor.getString(7);
                      if (value == null) value = ""; // prevent null pointer (however should the detail be even stored in People DB if null?)
                      // Key=6
                      final int key = cursor.isNull(6) ? ContactChange.KEY_UNKNOWN : mapInternalKeyToContactChangeKey(cursor.getInt(6));
                      // Type=4, OrderNo=5
                      final int flags = mapInternalTypeAndOrderToContactChangeFlag(cursor.isNull(4) ? 0 : cursor.getInt(4), cursor.getInt(5));
                      // create the change
                      final ContactChange change = new ContactChange(key, value, flags);
                      changes[index++] = change;
                      // LocalContactId=0
                      change.setInternalContactId(cursor.isNull(0) ? ContactChange.INVALID_ID : cursor.getLong(0));
                      // DetailLocalId=1
                      change.setInternalDetailId(cursor.isNull(1) ? ContactChange.INVALID_ID : cursor.getInt(1));
                      // NativeDetailId=2
                      change.setNabDetailId(cursor.isNull(2) ? ContactChange.INVALID_ID : cursor.getLong(2));
                      // NativeContactIdDup=3
                      change.setNabContactId(cursor.isNull(3) ? ContactChange.INVALID_ID : cursor.getLong(3));
                      // DetailServerId=8
                      change.setBackendDetailId(cursor.isNull(8) ? ContactChange.INVALID_ID : cursor.getLong(8));
                      
                      if (nativeSyncableOnly) {
                          
                          // in this mode, we have to tell if the detail is new or updated
                          if (change.getNabDetailId() == ContactChange.INVALID_ID) {
                              change.setType(ContactChange.TYPE_ADD_DETAIL);
                          } else {
                              change.setType(ContactChange.TYPE_UPDATE_DETAIL);
                          }
                      }
                  }
                  
                  if (index == changes.length) {
                      
                      return changes;
                  } else if (index > 0) {
                      
                      // there were some empty details, need to trim the array
                      final ContactChange[] trimmed = new ContactChange[index];
                      System.arraycopy(changes, 0, trimmed, 0, index);
                      
                      return trimmed;
                  }
              }
              
          } catch (Exception e) {
              
              // what else can we do?
              LogUtils.logE("ContactDetailsTable.getContactChanges(): " + e);
          }
          finally {
              
              CursorUtils.closeCursor(cursor);
          }
          
          return null;
      }
      
      /**
       * LocalId = ?
       */
      private final static String SQL_STRING_LOCAL_ID_EQUAL_QUESTION_MARK = Field.DETAILLOCALID + " = ?";
      
      /**
       * Sets the detail as synchronized with native side.
       * 
       * @param localContactId the local id of the detail
       * @param nativeContactId the native contact (only needed for added details i.e. isNewlySynced = true)
       * @param nativeDetailId the native detail id (only needed for added details i.e. isNewlySynced = true)
       * @param writableDb the db where to write
       * @param isNewlySynced true if the detail as not been synchronized before, false if this is an update
       * @return true if sucessful, false otherwise
       */
      public static boolean setDetailSyncedWithNative(long localDetailId, long nativeContactId, long nativeDetailId, boolean isNewlySynced, SQLiteDatabase writableDb) {
          
          final ContentValues values = new ContentValues();
          
          if (isNewlySynced) {
              
              // in the isNewlySynced mode, we need to sync the native ids as well
              values.put(Field.NATIVECONTACTID.toString(), nativeContactId);
              values.put(Field.NATIVEDETAILID.toString(), nativeDetailId);
          }
          // set the sync with native flag to done (i.e. -1)
          values.put(Field.NATIVESYNCCONTACTID.toString(), -1);
          
          try {
              
              if (writableDb.update(TABLE_NAME, values, SQL_STRING_LOCAL_ID_EQUAL_QUESTION_MARK, new String[] { Long.toString(localDetailId) }) == 1) {
                  
                  return true;
              }
              
          } catch (Exception e) {
              
              LogUtils.logE("ContactsTable.setNativeContactId() Exception - " + e);
          }
          
          return false;
      }
  }
  