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