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.List;
  
  import android.content.ContentValues;
  import android.database.Cursor;
  import android.database.SQLException;
  import android.database.sqlite.SQLiteDatabase;
  import android.database.sqlite.SQLiteException;
  import android.text.format.Time;
  
  import com.vodafone360.people.Settings;
  import com.vodafone360.people.database.DatabaseHelper;
  import com.vodafone360.people.datatypes.ContactDetail;
  import com.vodafone360.people.engine.contactsync.ContactChange;
  import com.vodafone360.people.utils.CloseUtils;
  import com.vodafone360.people.utils.LogUtils;
  
  /**
   * Contains all the functionality related to the native contact change log
   * database table. This table is used to persist database changes that have not
   * yet been synced to the native Android phonebook. This class is never
   * instantiated hence all methods must be static.
   * 
   * @version %I%, %G%
   */
  public abstract class NativeChangeLogTable {
      /**
       * Name of the table as it appears in the database
       */
      public static final String TABLE_NAME = "NativeChangeLog";
  
      /**
       * Name of a temporary table created and used to batch the deletion of sync
       * data from the change log table.
       */
      private static final String TEMP_CONTACT_CHANGE_TABLE = "SyncNativeInfo";
  
      /**
       * Primary key for the temporary table
       */
      private static final String TEMP_CONTACT_CHANGE_TABLE_ID = "Id";
  
      /**
       * Secondary key which links to the primary key of the change log table
       */
      private static final String TEMP_CONTACT_CHANGE_LOG_ID = "LogId";
      
      /**
       * Query string to get a list of deleted details for a provided type and local id.
       * 
       * @see #getDeletedDetails(long, SQLiteDatabase)
       */
      private final static String QUERY_DELETED_DETAILS = 
          "SELECT " + Field.CHANGETYPE + ", " + Field.LOCALCONTACTID + ", " + Field.NATIVECONTACTID +
          ", " + Field.LOCALDETAILID + ", " + Field.NATIVEDETAILID + ", " + Field.DETAILKEY + 
          " FROM " + TABLE_NAME + " WHERE " + Field.LOCALCONTACTID + " = ? AND " + Field.CHANGETYPE + " = ?";
  
      /**
       * SELECT NativeContactId FROM NativeChangeLog WHERE ChangeType = 0 ORDER BY NativeContactId
       */
      public final static String QUERY_MODIFIED_CONTACTS_LOCAL_IDS = 
      	"SELECT DISTINCT " + Field.LOCALCONTACTID + " FROM " + TABLE_NAME + " ORDER BY " + Field.LOCALCONTACTID;
      
      /**
       * 
       */
      public final static String QUERY_MODIFIED_CONTACTS_LOCAL_IDS_NO_ORDERBY = 
      	"SELECT " + Field.LOCALCONTACTID + " FROM " + TABLE_NAME;
  
      /**
       * An enumeration of all the field names in the database.
       */
      private static enum Field {
          NATIVECHANGEID("NativeChangeId"),
          CHANGETYPE("ChangeType"),
          LOCALCONTACTID("LocalContactId"),
          NATIVECONTACTID("NativeContactId"),
          LOCALDETAILID("LocalDetailId"),
          NATIVEDETAILID("NativeDetailId"),
          DETAILKEY("DetailKey"),
          TIMESTAMP("Timestamp");
  
          /**
           * The name of the field as it appears in the database
           */
          private String mField;
  
          /**
           * Constructor
           * 
           * @param field - The name of the field (see list above)
           */
          private Field(String field) {
              mField = field;
          }
  
          /**
           * @return the name of the field as it appears in the database.
           */
          public String toString() {
              return mField;
          }
  
      }
  
      /**
       * Enumerates the type of changes supported by this table, including:
       * <ul>
       * <li>DELETE_CONTACT: A contact has been deleted from the database and
       * needs to be deleted in the native. The local Contact ID, native Contact
       * ID fields are required</li>
       * <li>DELETE_DETAIL: A contact detail has been deleted from the database
       * and needs to be deleted from the native. The local Contact ID, native
       * Contact ID, local Detail ID, native Detail ID (if the detail has one) and
       * detail key are required</li> </li>
       * </ul>
       */
      public static enum ContactChangeType {
          DELETE_CONTACT,
          DELETE_DETAIL
      }
  
      /**
       * Wraps up the data present in the change log table
       */
      public static class ContactChangeInfo {
          public Long mNativeChangeId = null;
  
          public ContactChangeType mType = null;
  
          public Long mLocalContactId = null;
  
          public Integer mNativeContactId = null;
  
          public Long mLocalDetailId = null;
  
          public Integer mNativeDetailId = null;
  
          public ContactDetail.DetailKeys mDetailKey = null;
  
          /**
           * Converts the encapsulated data into a string that can be displayed
           * for debug purposes.
           */
          @Override
          public String toString() {
              return "Contact Change ID: " + mNativeChangeId + "\n" + "Contact Change Type: " + mType
                      + "\n" + "Local Contact ID: " + mLocalContactId + "\n" + "Native Contact ID: "
                      + mNativeContactId + "\n" + "Local Detail ID: " + mLocalDetailId;
          }
      }
  
      /**
       * Create Server Change Log Table.
       * 
       * @param writeableDb A writable SQLite database
       * @throws SQLException If an SQL compilation error occurs
       */
      public static void create(SQLiteDatabase writableDb) throws SQLException {
          DatabaseHelper.trace(true, "NativeChangeLogTable.create()");
          writableDb.execSQL("CREATE TABLE " + TABLE_NAME + " (" + Field.NATIVECHANGEID
                  + " INTEGER PRIMARY KEY, " + Field.CHANGETYPE + " INTEGER NOT NULL, "
                  + Field.LOCALCONTACTID + " LONG NOT NULL, " + Field.NATIVECONTACTID + " LONG, "
                  + Field.LOCALDETAILID + " LONG, " + Field.NATIVEDETAILID + " INTEGER, "
                  + Field.DETAILKEY + " INTEGER, " + Field.TIMESTAMP + " DATE);");
      }
  
      /**
       * Fetches a comma separated list of table fields which can be used in an
       * SQL SELECT statement as the query projection. The
       * {@link #getQueryData(Cursor)} method can used to fetch data from the
       * cursor.
       * 
       * @param whereClause A where clause (without the where) to filter the
       *            result. Cannot be null.
       * @return SQL string
       */
      private static String getQueryStringSql(String whereClause) {
          DatabaseHelper.trace(false, "NativeChangeLogTable.getQueryStringSql()");
          String whereClauseFull = "";
          if (whereClause != null) {
              whereClauseFull = " WHERE " + whereClause;
          }
          return "SELECT " + Field.NATIVECHANGEID + ", " + Field.CHANGETYPE + ", "
                  + Field.LOCALCONTACTID + ", " + Field.NATIVECONTACTID + ", " + Field.LOCALDETAILID
                  + ", " + Field.NATIVEDETAILID + ", " + Field.DETAILKEY + ", " + Field.TIMESTAMP
                  + " FROM " + TABLE_NAME + whereClauseFull;
      }
  
      /**
       * Column indices for database queries
       */
      private static final int NATIVECHANGEID = 0;
  
      private static final int TYPE = 1;
  
      private static final int LOCALCONTACTID = 2;
  
      private static final int NATIVECONTACTID = 3;
  
      private static final int LOCALDETAILID = 4;
  
      private static final int NATIVEDETAILID = 5;
  
      private static final int DETAILKEY = 6;
  
      /**
       * Fetches change log information from a cursor at the current position. The
       * {@link #getQueryStringSql(String)} method should be used to make the
       * query.
       * 
       * @param c The cursor from the query
       * @return A filled in ContactChangeInfo object
       */
      public static ContactChangeInfo getQueryData(Cursor c) {
          DatabaseHelper.trace(false, "NativeChangeLogTable.getQueryData()");
          ContactChangeInfo info = new ContactChangeInfo();
          if (!c.isNull(NATIVECHANGEID)) {
              info.mNativeChangeId = c.getLong(NATIVECHANGEID);
          }
          if (!c.isNull(TYPE)) {
              final int typeIdx = c.getInt(TYPE);
              if (typeIdx < ContactChangeType.values().length) {
                  info.mType = ContactChangeType.values()[typeIdx];
              }
          }
          if (!c.isNull(LOCALCONTACTID)) {
              info.mLocalContactId = c.getLong(LOCALCONTACTID);
          }
          if (!c.isNull(NATIVECONTACTID)) {
              info.mNativeContactId = c.getInt(NATIVECONTACTID);
          }
          if (!c.isNull(LOCALDETAILID)) {
              info.mLocalDetailId = c.getLong(LOCALDETAILID);
          }
          if (!c.isNull(NATIVEDETAILID)) {
              info.mNativeDetailId = c.getInt(NATIVEDETAILID);
          }
          if (!c.isNull(DETAILKEY)) {
              final int keyIdx = c.getInt(DETAILKEY);
              if (keyIdx < ContactDetail.DetailKeys.values().length) {
                  info.mDetailKey = ContactDetail.DetailKeys.values()[keyIdx];
              }
          }
          // Ignore timestamp
          return info;
      }
  
      /**
       * Inserts "Delete Contact" operation into the change log.
       * 
       * @param localContactId Local contact ID from the Contacts table
       * @param nativeContactId Native contact ID. If this is null it is assumed
       *            that the contact hasn't yet been synced with the native
       *            phonebook, so the new contact change is removed from the log
       *            and the function returns.
       * @param writableDb Writable SQLite database
       * @return true if successful, false otherwise
       */
      public static boolean addDeletedContactChange(Long localContactId, Integer nativeContactId,
              SQLiteDatabase writableDb) {
          DatabaseHelper.trace(false, "NativeChangeLogTable.addDeletedContactChange()");
          if (localContactId == null) {
              LogUtils.logE("NativeChangeLogTable.addDeletedContactChange() Invalid parameter");
              return false;
          }
          boolean addToLog = true;
          if (nativeContactId == null || nativeContactId == -1 || nativeContactId == 0) {
              // FIXME: invalid native id, no need to add it to the table. Why is this happening???
              addToLog = false;
          }
          removeContactChanges(localContactId, writableDb);
          if (!addToLog) {
              return true;
          }
  
          ContactChangeInfo info = new ContactChangeInfo();
          info.mType = ContactChangeType.DELETE_CONTACT;
          info.mLocalContactId = localContactId;
          info.mNativeContactId = nativeContactId;
          if (!addContactChange(info, writableDb)) {
              return false;
          }
          return true;
      }
  
      /**
       * Inserts "Delete Contact Detail" operation into the change log.
       * 
       * @param detail Must have a valid local contact ID, local detail ID and
       *            key. Will not be added to the change log if the contact native
       *            ID is null.
       * @param writableDb Writable SQLite database
       * @return true if successful, false otherwise
       */
      public static boolean addDeletedContactDetailChange(ContactDetail detail,
              SQLiteDatabase writableDb) {
          DatabaseHelper.trace(false, "NativeChangeLogTable.addDeletedContactDetailChange()");
  
          if (detail.localContactID == null || detail.localDetailID == null || detail.key == null) {
              LogUtils.logE("NativeChangeLogTable.addDeletedContactDetailChange() Invalid parameter");
              return false;
          }
          if (isContactChangeInList(detail.localContactID, ContactChangeType.DELETE_CONTACT,
                  writableDb)) {
              LogUtils
                      .logE("NativeChangeLogTable.addDeletedContactDetailChange() Associated contact has already been deleted (#1)");
              return false;
          }
          if (isContactDetailChangeInList(detail.localDetailID, ContactChangeType.DELETE_DETAIL,
                  writableDb)) {
              LogUtils
                      .logE("NativeChangeLogTable.addDeletedContactDetailChange() Associated contact detail has already been deleted (#2)");
              return false;
          }
          removeContactDetailChanges(detail.localDetailID, writableDb);
          ContactChangeInfo info = new ContactChangeInfo();
          info.mLocalContactId = detail.localContactID;
          info.mNativeContactId = detail.nativeContactId;
          info.mLocalDetailId = detail.localDetailID;
          info.mType = ContactChangeType.DELETE_DETAIL;
          info.mDetailKey = detail.key;
          info.mNativeDetailId = detail.nativeDetailId;
  
          if (info.mNativeContactId == null || info.mNativeContactId == -1) {
  
              // FIXME: this shall not happen (has been potentially fixed) but currently guarding against it...
              // This line will be removed later when the root cause is completely identified
              LogUtils.logE("==========================================================================================================================");
              LogUtils.logE("NativeChangeLogTable.addDeletedContactDetailChange(): a detail without a native contact id can't be added to the table!!!!");
              LogUtils.logE("==========================================================================================================================");
              return true;
          }
  
          if (!addContactChange(info, writableDb)) {
              return false;
          }
          return true;
      }
  
      /**
       * Inserts a contact change into the table.
       * 
       * @param info contact change info. This must have a valid type field and
       *            depending on type other fields may also be required (see
       *            {@link ContactChangeType}).
       * @param writableDb Writable SQLite database
       * @return true if successful, false otherwise
       */
      private static boolean addContactChange(ContactChangeInfo info, SQLiteDatabase writableDb) {
          DatabaseHelper.trace(true, "NativeChangeLogTable.addContactChange()");
          try {
              ContentValues changeValues = new ContentValues();
              changeValues.put(Field.CHANGETYPE.toString(), info.mType.ordinal());
              changeValues.put(Field.LOCALCONTACTID.toString(), info.mLocalContactId);
              if (info.mDetailKey != null) {
                  changeValues.put(Field.DETAILKEY.toString(), info.mDetailKey.ordinal());
              }
              if (info.mNativeContactId != null) {
                  changeValues.put(Field.NATIVECONTACTID.toString(), info.mNativeContactId);
              }
              if (info.mNativeDetailId != null) {
                  changeValues.put(Field.NATIVEDETAILID.toString(), info.mNativeDetailId);
              }
              if (info.mLocalDetailId != null) {
                  changeValues.put(Field.LOCALDETAILID.toString(), info.mLocalDetailId);
              }
              Time time = new Time();
              time.setToNow();
              changeValues.put(Field.TIMESTAMP.toString(), time.format2445());
              long id = writableDb.insertOrThrow(TABLE_NAME, null, changeValues);
              if (id < 0) {
                  LogUtils
                          .logE("NativeChangeLogTable.addContactChange() Unable to add contact change to log table - a database error has occurred");
                  return false;
              }
              info.mNativeChangeId = id;
              return true;
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "NativeChangeLogTable.addContactChange() SQLException - Unable to add contact change to log table",
                              e);
              return false;
          }
      }
  
      /**
       * Determines if a specific contact and change type exists in the change
       * log.
       * 
       * @param localContactId Local contact ID from the Contacts table
       * @param type The change type to find
       * @param readableDb Readable SQLite database
       * @return true if the change is found, false otherwise
       */
      public static boolean isContactChangeInList(Long localContactId, ContactChangeType type,
              SQLiteDatabase readableDb) {
          DatabaseHelper.trace(false, "NativeChangeLogTable.isContactChangeInList()");
          Cursor c = null;
          try {
              c = readableDb.rawQuery("SELECT " + Field.LOCALCONTACTID + " FROM " + TABLE_NAME
                      + " WHERE " + Field.LOCALCONTACTID + "=" + localContactId + " AND "
                      + Field.CHANGETYPE + "=" + type.ordinal(), null);
              if (c.moveToFirst()) {
                  return true;
              }
              return false;
          } catch (SQLException e) {
              return false;
          } finally {
              CloseUtils.close(c);
              c = null;
          }
      }
      
      /**
       * 
       * @param nativeContactId
       * @param type
       * @param readableDb
       * @return
       */
      public static boolean isContactChangeInList(long nativeContactId, ContactChangeType type,
              SQLiteDatabase readableDb) {
          DatabaseHelper.trace(false, "NativeChangeLogTable.isContactChangeInList()");
          Cursor c = null;
          try {
              c = readableDb.rawQuery("SELECT " + Field.LOCALCONTACTID + " FROM " + TABLE_NAME
                      + " WHERE " + Field.NATIVECONTACTID + "=" + nativeContactId + " AND "
                      + Field.CHANGETYPE + "=" + type.ordinal(), null);
              if (c.moveToFirst()) {
                  return true;
              }
              return false;
          } catch (SQLException e) {
              return false;
          } finally {
              CloseUtils.close(c);
              c = null;
          }
      }
  
      /**
       * Determines if a specific contact detail and change type exists in the
       * change log.
       * 
       * @param localDetailId Local contact detail ID from ContactDetail table
       * @param type The change type to find
       * @param readableDb Readable SQLite database
       * @return true if the change is found, false otherwise
       */
      private static boolean isContactDetailChangeInList(Long localDetailId, ContactChangeType type,
              SQLiteDatabase readableDb) {
          DatabaseHelper.trace(false, "NativeChangeLogTable.isContactDetailChangeInList()");
          Cursor c = null;
          try {
              c = readableDb.rawQuery("SELECT " + Field.LOCALDETAILID + " FROM " + TABLE_NAME
                      + " WHERE " + Field.LOCALDETAILID + "=" + localDetailId + " AND "
                      + Field.CHANGETYPE + "=" + type.ordinal(), null);
              if (c.moveToFirst()) {
                  return true;
              }
              return false;
          } catch (SQLException e) {
              return false;
          } finally {
              CloseUtils.close(c);
              c = null;
          }
      }
  
      /**
       * Removes all changes from the change log associated with a particular
       * contact.
       * 
       * @param localContactId Local contact ID from the Contacts table
       * @param writableDb Writable SQLite database
       * @return true if successful, false otherwise
       */
      public static boolean removeContactChanges(Long localContactId, SQLiteDatabase writableDb) {
          DatabaseHelper.trace(true, "NativeChangeLogTable.removeContactChanges()");
          try {
              int result = writableDb.delete(TABLE_NAME, Field.LOCALCONTACTID + "=" + localContactId,
                      null);
              if (result <= 0) {
                  return false;
              }
          } catch (SQLException e) {
              return false;
          }
          return true;
      }
  
      /**
       * Removes all changes from the change log associated with a particular
       * contact detail.
       * 
       * @param localDetailId Local contact detail ID from the Contact details
       *            table
       * @param writableDb Writable SQLite database
       * @return true if successful, false otherwise
       */
      public static boolean removeContactDetailChanges(Long localDetailId, SQLiteDatabase writableDb) {
          DatabaseHelper.trace(true, "NativeChangeLogTable.removeContactDetailChanges()");
          try {
              if (writableDb.delete(TABLE_NAME, Field.LOCALDETAILID + "=" + localDetailId, null) < 0) {
                  return false;
              }
              // When 0 rows are deleted we still should return success
          } catch (SQLException e) {
              return false;
          }
          return true;
      }
  
      /**
       * Fetches the number of changes listed in the table.
       * 
       * @param type The type of change to count, if null all change types are
       *            included
       * @param readableDb Readable SQLite database
       * @return The number of records
       */
      public static long fetchNoOfChanges(ContactChangeType type, SQLiteDatabase readableDb) {
          DatabaseHelper.trace(false, "NativeChangeLogTable.fetchNoOfChanges()");
          long noOfChanges = 0;
          Cursor c = null;
          try {
              String query = "SELECT COUNT(*) FROM " + TABLE_NAME;
              if (type != null) {
                  query += " WHERE " + Field.CHANGETYPE.toString() + "=" + type.ordinal();
              }
              c = readableDb.rawQuery(query, null);
              if (c.moveToFirst()) {
                  noOfChanges = c.getLong(0);
              }
          } catch (SQLException e) {
              LogUtils
                      .logE(
                              "NativeChangeLogTable.fetchNoOfChanges() SQLException - Unable to fetch changes",
                              e);
          } finally {
              CloseUtils.close(c);
              c = null;
          }
          return noOfChanges;
      }
  
      /**
       * Removes a list of changes from the change log table. This method is used
       * once the native has been successfully updated by the client.
       * 
       * @param changeIdList A list of contact change IDs (can be obtained from
       *            {@link ContactChangeInfo#mNativeChangeId}.
       * @param writableDb Writable SQLite database
       * @return true if successful, false otherwise
       */
      public static boolean syncDeleteNativeChangeLog(List<Long> changeIdList,
              SQLiteDatabase writableDb) {
          DatabaseHelper.trace(true, "NativeChangeLogTable.syncDeleteNativeChangeLog()");
          
          try {
              writableDb.beginTransaction();
              writableDb.execSQL("DROP TABLE IF EXISTS " + TEMP_CONTACT_CHANGE_TABLE);
              writableDb.execSQL("CREATE TEMPORARY TABLE " + TEMP_CONTACT_CHANGE_TABLE + "("
                      + TEMP_CONTACT_CHANGE_TABLE_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
                      + TEMP_CONTACT_CHANGE_LOG_ID + " LONG);");
              ContentValues values = new ContentValues();
              for (Long changeId : changeIdList) {
                  values.put(TEMP_CONTACT_CHANGE_LOG_ID, changeId);
                  if (writableDb.insertOrThrow(TEMP_CONTACT_CHANGE_TABLE, null, values) < 0) {
                      return false;
                  }
              }
              writableDb.execSQL("DELETE FROM " + TABLE_NAME + " WHERE " + Field.NATIVECHANGEID
                      + " IN (SELECT " + TEMP_CONTACT_CHANGE_LOG_ID + " FROM "
                      + TEMP_CONTACT_CHANGE_TABLE + ");");
              writableDb.setTransactionSuccessful();
              return true;
          } catch (SQLException e) {
              LogUtils.logE("NativeChangeLogTable.syncDeleteNativeChangeLog() "
                      + "SQLException - Unable to remove contact detail change from log", e);
              return false;
          } finally {
              writableDb.endTransaction();
          }
      }
  
      /**
       * Fetches a list of changes from the table for a specific type. The list is
       * ordered by local contact ID.
       * 
       * @param type The type of change to return (cannot be null)
       * @param readableDb Readable SQLite database
       * @return A cursor for use with {@link #getQueryData(Cursor)} if
       *         successful, null otherwise
       */
      public static Cursor fetchContactChangeLogCursor(ContactChangeType type,
              SQLiteDatabase readableDb) {
          DatabaseHelper.trace(false, "NativeChangeLogTable.fetchContactChangeLogCursor()");
          try {
              return readableDb.rawQuery(getQueryStringSql(Field.CHANGETYPE + "=" + type.ordinal()
                      + " ORDER BY " + Field.LOCALCONTACTID), null);
          } catch (SQLiteException e) {
              LogUtils.logE("NativeChangeLogTable.fetchContactChangeLogCursor() "
                      + "Exception - Unable to fetch native change log cursor", e);
              return null;
          }
      }
  
      /**
       * Fetches a list of changes from the table for a specific type. The list is
       * ordered by local contact ID.
       * 
       * @param type The type of change to return
       * @param contactChangeList The list to be populated
       * @param readableDb Readable SQLite database
       * @return true if successful, false otherwise
       */
      public static boolean fetchContactChangeLog(ContactChangeType type,
              List<ContactChangeInfo> nativeInfoList, SQLiteDatabase readableDb) {
          DatabaseHelper.trace(false, "NativeChangeLogTable.fetchContactChangeLog()");
          Cursor c = null;
          try {
              nativeInfoList.clear();
              String whereClause = null;
              if (type != null) {
                  whereClause = Field.CHANGETYPE + "=" + type.ordinal();
              }
  
              c = readableDb.rawQuery(getQueryStringSql(whereClause) + " ORDER BY "
                      + Field.NATIVECONTACTID, null);
              while (c.moveToNext()) {
                  nativeInfoList.add(getQueryData(c));
              }
              return true;
          } catch (SQLiteException e) {
              LogUtils.logE("NativeChangeLogTable.fetchContactChangeLog() "
                      + "Exception - Unable to fetch native change log", e);
              return false;
          } finally {
              CloseUtils.close(c);
              c = null;
          }
      }
      
      /**
       * 
       * @param readableDb
       * @return
       */
      public static long[] getModifiedContactsLocalIds(SQLiteDatabase readableDb) {
          
          long[] ids = null;
          Cursor cursor = null;
          
          try {
              
              final int LOCAL_ID_INDEX = 0;
              
              cursor = readableDb.rawQuery(QUERY_MODIFIED_CONTACTS_LOCAL_IDS, null);
              
              if (cursor.getCount() > 0) {
                  
                  int i = 0;
                  ids = new long[cursor.getCount()];
                  
                  while (cursor.moveToNext()) {
                      ids[i++] = cursor.getInt(LOCAL_ID_INDEX);
                  }
              } else {
                  
                  return null;
              }
          } catch (Exception e) {
              
              if (Settings.ENABLED_DATABASE_TRACE) {
                  DatabaseHelper.trace(false, "getModifiedContactsNativeIds(): "+e);
              }
          } finally {
              
              CloseUtils.close(cursor);
              cursor = null;
          }
          
          return ids;
      }
      
      /**
       * SELECT NativeContactId FROM NativeChangeLog WHERE ChangeType = 0 ORDER BY NativeContactId
       */
      private final static String QUERY_DELETED_CONTACTS_NATIVE_IDS = "SELECT " + Field.NATIVECONTACTID + " FROM " + TABLE_NAME + " WHERE "
                                                                    + Field.CHANGETYPE + " = " + ContactChangeType.DELETE_CONTACT.ordinal()
                                                                    + " ORDER BY " + Field.NATIVECONTACTID;
      
      /**
       * 
       */
      private final static String QUERY_DELETED_CONTACT_NATIVE_ID = "SELECT " + Field.NATIVECONTACTID + " FROM " + TABLE_NAME + " WHERE "
                                                                    + Field.CHANGETYPE + " = " + ContactChangeType.DELETE_CONTACT.ordinal()
                                                                    + " AND " + Field.LOCALCONTACTID + " = ?";
      /**
       * Gets the list of native ids for the deleted contacts.
       *
       * @param readableDb the db to query from
       * @return an array of long ids of deleted native contacts, null if none
       */
      public static long[] getDeletedContactsNativeIds(SQLiteDatabase readableDb) {
          
          long[] ids = null;
          Cursor cursor = null;
          
          try {
              
              final int NATIVE_ID_INDEX = 0;
              
              cursor = readableDb.rawQuery(QUERY_DELETED_CONTACTS_NATIVE_IDS, null);
              
              if (cursor.getCount() > 0) {
                  
                  int i = 0;
                  ids = new long[cursor.getCount()];
                  
                  while (cursor.moveToNext()) {
                      ids[i++] = cursor.getInt(NATIVE_ID_INDEX);
                  }
              } else {
                  
                  return null;
              }
          } catch (Exception e) {
              
              if (Settings.ENABLED_DATABASE_TRACE) {
                  DatabaseHelper.trace(false, "NativeChangeLogTable.getDeletedContactsNativeIds(): "+e);
              }
          } finally {
              
              CloseUtils.close(cursor);
              cursor = null;
          }
          
          return ids;
      }
      
      /**
       * Gets the list of deleted details as a ContactChange array.
       * 
       * Note: the ContactChange object will have the type ContactChange.TYPE_DELETE_DETAIL
       * 
       * @see ContactChange
       * 
       * @param localContactId the local contact id to query
       * @param readableDb the db to query from
       * @return an array of ContactChange, null if no details where found
       */
      public static ContactChange[] getDeletedDetails(long localContactId, SQLiteDatabase readableDb) {
          
          final String[] SELECTION = { String.valueOf(localContactId), Integer.toString(ContactChangeType.DELETE_DETAIL.ordinal()) };
          Cursor cursor = null;
          
          try {
              
              cursor = readableDb.rawQuery(QUERY_DELETED_DETAILS, SELECTION);
              
              if (cursor.getCount() > 0) {
                  
                  final ContactChange[] deletedDetails = new ContactChange[cursor.getCount()];
                  int index = 0;
                  while (cursor.moveToNext()) {
                      
                      // fill the ContactChange class with contact detail data
                      final ContactChange change = new ContactChange();
                      deletedDetails[index++] = change;
                      // set the ContactChange as a deleted contact detail
                      change.setType(ContactChange.TYPE_DELETE_DETAIL);
                      // LocalContactId=1
                      change.setInternalContactId(cursor.isNull(1) ? ContactChange.INVALID_ID : cursor.getLong(1));
                      // NativeContactId=2
                      change.setNabContactId(cursor.isNull(2) ? ContactChange.INVALID_ID : cursor.getLong(2));
                      // DetailLocalId=3
                      change.setInternalDetailId(cursor.isNull(3) ? ContactChange.INVALID_ID : cursor.getInt(3));
                      // NativeDetailId=4
                      change.setNabDetailId(cursor.isNull(4) ? ContactChange.INVALID_ID : cursor.getLong(4));
                      // DetailKey=5
                      change.setKey(cursor.isNull(5) ? ContactChange.KEY_UNKNOWN : ContactDetailsTable.mapInternalKeyToContactChangeKey(cursor.getInt(5)));
                      
                      if (change.getNabContactId() == ContactChange.INVALID_ID) {
                          
                          LogUtils.logE("NativeChangeLogTable.getDeletedDetails(): the native contact id shall not be null! cc.internalContactId="+change.getInternalContactId()+", cc.key="+change.getKey());
                      }
                  }
                  
                  return deletedDetails;
              }
          } catch (Exception e) {
              
              if (Settings.ENABLED_DATABASE_TRACE) {
                  DatabaseHelper.trace(false, "NativeChangeLogTable.getDeletedContactsNativeIds(): "+e);
              }
          } finally {
              
              CloseUtils.close(cursor);
              cursor = null;
          }
          
          return null;
      }
      
      /**
       * 
       * @param localContactId
       * @param readableDb
       * @return
       */
      public static long getDeletedContactNativeId(long localContactId, SQLiteDatabase readableDb) {
          
          final String[] SELECTION = { String.valueOf(localContactId) };
          Cursor cursor = null;
          
          try {
              
              cursor = readableDb.rawQuery(QUERY_DELETED_CONTACT_NATIVE_ID, SELECTION);
              
              if (cursor.getCount() > 0) {
                  
                  cursor.moveToNext();
                  final Long nativeId = cursor.getLong(0);
                  
                  return nativeId == null ? -1 : nativeId;
              }
          } catch (Exception e) {
              
              if (Settings.ENABLED_DATABASE_TRACE) {
                  DatabaseHelper.trace(false, "NativeChangeLogTable.getDeletedContactsNativeIds(): "+e);
              }
          } finally {
              
              CloseUtils.close(cursor);
              cursor = null;
          }
          
          return -1;
      }
  }
  