/* * 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.HashMap; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Map.Entry; 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.tables.ContactsTable.ContactIdInfo; import com.vodafone360.people.datatypes.Contact; import com.vodafone360.people.datatypes.ContactDetail; import com.vodafone360.people.datatypes.ContactSummary; import com.vodafone360.people.datatypes.VCardHelper; import com.vodafone360.people.datatypes.ContactDetail.DetailKeys; import com.vodafone360.people.datatypes.ContactSummary.AltFieldType; import com.vodafone360.people.datatypes.ContactSummary.OnlineStatus; import com.vodafone360.people.engine.meprofile.SyncMeDbUtils; import com.vodafone360.people.engine.presence.User; import com.vodafone360.people.service.ServiceStatus; import com.vodafone360.people.utils.CloseUtils; import com.vodafone360.people.utils.LogUtils; import com.vodafone360.people.utils.StringBufferPool; /** * The ContactSummaryTable contains a summary of important contact details for * each contact such as name, status and Avatar availability. This data is * duplicated here to improve the performance of the main contact list in the UI * (otherwise the a costly inner join between the contact and contact details * table would be needed). This class is never instantiated hence all methods * must be static. * * @version %I%, %G% */ public abstract class ContactSummaryTable { /** * The name of the table as it appears in the database. */ public static final String TABLE_NAME = "ContactSummary"; public static final String TABLE_INDEX_NAME = "ContactSummaryIndex"; /** * SQL localized collate for sorting contact list. */ private static final String LOCALIZED_COLLATE = " COLLATE LOCALIZED ASC"; /** * This holds the presence information for each contact in the ContactSummaryTable */ private static HashMap<Long, Integer> sPresenceMap = new HashMap<Long, Integer>(); /** * An enumeration of all the field names in the database. */ public static enum Field { SUMMARYID("_id"), LOCALCONTACTID("LocalContactId"), DISPLAYNAME("DisplayName"), STATUSTEXT("StatusText"), ALTFIELDTYPE("AltFieldType"), ALTDETAILTYPE("AltDetailType"), ONLINESTATUS("OnlineStatus"), NATIVEID("NativeId"), FRIENDOFMINE("FriendOfMine"), PICTURELOADED("PictureLoaded"), SNS("Sns"), SYNCTOPHONE("Synctophone"), SEARCHNAME("Searchname"); /** * The name of the field as it appears in the database */ private final 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; } } /** * Creates ContactSummary Table. * * @param writeableDb A writable SQLite database * @throws SQLException If an SQL compilation error occurs */ public static void create(SQLiteDatabase writeableDb) throws SQLException { DatabaseHelper.trace(true, "ContactSummaryTable.create()"); //TODO: As of now kept the onlinestatus field in table. Would remove it later on /** * Field.SEARCHNAME is added into the ContactSummary for searching the contacts case * insensitively when Field.DISPLAYNAME is having special characters like à, è, ù, â, ê, î, ô. */ writeableDb.execSQL("CREATE TABLE " + TABLE_NAME + " (" + Field.SUMMARYID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + Field.LOCALCONTACTID + " LONG, " + Field.DISPLAYNAME + " TEXT, " + Field.STATUSTEXT + " TEXT, " + Field.ALTFIELDTYPE + " INTEGER, " + Field.ALTDETAILTYPE + " INTEGER, " + Field.ONLINESTATUS + " INTEGER, " + Field.NATIVEID + " INTEGER, " + Field.FRIENDOFMINE + " BOOLEAN, " + Field.PICTURELOADED + " BOOLEAN, " + Field.SNS + " STRING, " + Field.SYNCTOPHONE + " BOOLEAN, "+ Field.SEARCHNAME + " TEXT);"); writeableDb.execSQL("CREATE INDEX " + TABLE_INDEX_NAME + " ON " + TABLE_NAME + " ( " + Field.LOCALCONTACTID + ", " + Field.DISPLAYNAME + " )"); clearPresenceMap(); } /** * 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 * @see #getQueryData(Cursor). */ private static String getFullQueryList() { return Field.SUMMARYID + ", " + TABLE_NAME + "." + Field.LOCALCONTACTID + ", " + Field.DISPLAYNAME + ", " + Field.STATUSTEXT + ", " + Field.ONLINESTATUS + ", " + Field.NATIVEID + ", " + Field.FRIENDOFMINE + ", " + Field.PICTURELOADED + ", " + Field.SNS + ", " + Field.SYNCTOPHONE + ", " + Field.ALTFIELDTYPE + ", " + Field.ALTDETAILTYPE; } /** * Returns a full SQL query statement to fetch the contact summary * information. The {@link #getQueryData(Cursor)} method can be used to * obtain the data from the query. * * @param whereClause An SQL where clause (without the "WHERE"). Cannot be * null. * @return The query string * @see #getQueryData(Cursor). */ private static String getQueryStringSql(String whereClause) { return "SELECT " + getFullQueryList() + " FROM " + TABLE_NAME + " WHERE " + whereClause; } /** * UPDATE ContactSummary SET * NativeId = ? * WHERE LocalContactId = ? */ private static final String UPDATE_NATIVE_ID_BY_LOCAL_CONTACT_ID = "UPDATE " + TABLE_NAME + " SET " + Field.NATIVEID + "=? WHERE " + Field.LOCALCONTACTID + "=?"; /** * Column indices which match the query string returned by * {@link #getFullQueryList()}. */ public static final int SUMMARY_ID = 0; public static final int LOCALCONTACT_ID = 1; public static final int FORMATTED_NAME = 2; public static final int STATUS_TEXT = 3; @Deprecated public static final int ONLINE_STATUS = 4; public static final int NATIVE_CONTACTID = 5; public static final int FRIEND_MINE = 6; public static final int PICTURE_LOADED = 7; public static final int SNS = 8; public static final int SYNCTOPHONE = 9; public static final int ALTFIELD_TYPE = 10; public static final int ALTDETAIL_TYPE = 11; /** * Fetches the contact summary data from the current record of the given * cursor. * * @param c Cursor returned by one of the {@link #getFullQueryList()} based * query methods. * @return Filled in ContactSummary object */ public static ContactSummary getQueryData(Cursor c) { ContactSummary contactSummary = new ContactSummary(); if (!c.isNull(SUMMARY_ID)) { contactSummary.summaryID = c.getLong(SUMMARY_ID); } if (!c.isNull(LOCALCONTACT_ID)) { contactSummary.localContactID = c.getLong(LOCALCONTACT_ID); } contactSummary.formattedName = c.getString(FORMATTED_NAME); contactSummary.statusText = c.getString(STATUS_TEXT); contactSummary.onlineStatus = getPresence(contactSummary.localContactID); if (!c.isNull(NATIVE_CONTACTID)) { contactSummary.nativeContactId = c.getInt(NATIVE_CONTACTID); } if (!c.isNull(FRIEND_MINE)) { contactSummary.friendOfMine = (c.getInt(FRIEND_MINE) == 0 ? false : true); } if (!c.isNull(PICTURE_LOADED)) { contactSummary.pictureLoaded = (c.getInt(PICTURE_LOADED) == 0 ? false : true); } if (!c.isNull(SNS)) { contactSummary.sns = c.getString(SNS); } if (!c.isNull(SYNCTOPHONE)) { contactSummary.synctophone = (c.getInt(SYNCTOPHONE) == 0 ? false : true); } if (!c.isNull(ALTFIELD_TYPE)) { int val = c.getInt(ALTFIELD_TYPE); if (val < AltFieldType.values().length) { contactSummary.altFieldType = AltFieldType.values()[val]; } } if (!c.isNull(ALTDETAIL_TYPE)) { int val = c.getInt(ALTDETAIL_TYPE); if (val < ContactDetail.DetailKeys.values().length) { contactSummary.altDetailType = ContactDetail.DetailKeyTypes.values()[val]; } } return contactSummary; } /** * Fetches the contact summary for a particular contact * * @param localContactID The primary key ID of the contact to find * @param summary A new ContactSummary object to be filled in * @param readableDb Readable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus fetchSummaryItem(long localContactId, ContactSummary summary, SQLiteDatabase readableDb) { if (Settings.ENABLED_DATABASE_TRACE) { DatabaseHelper.trace(false, "ContactSummeryTable.fetchSummaryItem() localContactId[" + localContactId + "]"); } Cursor c1 = null; try { c1 = readableDb.rawQuery(getQueryStringSql(Field.LOCALCONTACTID + "=" + localContactId), null); if (!c1.moveToFirst()) { LogUtils.logW("ContactSummeryTable.fetchSummaryItem() localContactId[" + localContactId + "] not found in ContactSummeryTable."); return ServiceStatus.ERROR_NOT_FOUND; } summary.copy(getQueryData(c1)); return ServiceStatus.SUCCESS; } catch (SQLiteException e) { LogUtils.logE("ContactSummeryTable.fetchSummaryItem() Exception - Unable to fetch contact summary", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } finally { CloseUtils.close(c1); c1 = null; } } /** * Processes a ContentValues object to handle a missing name or missing * status. * <ol> * <li>If the name is missing it will be replaced using the alternative * detail.</li> * <li>If the name is present, but status is missing the status will be * replaced using the alternative detail</li> * <li>Otherwise, the althernative detail is not used</li> * </ol> * In any case the {@link Field#ALTFIELDTYPE} value will be updated to * reflect how the alternative detail is being used. * * @param values The ContentValues object to be updated * @param altDetail The must suitable alternative detail (see * {@link #fetchNewAltDetail(long, ContactDetail, SQLiteDatabase)} */ private static void updateAltValues(ContentValues values, ContactDetail altDetail) { if (!values.containsKey(Field.DISPLAYNAME.toString())) { values.put(Field.DISPLAYNAME.toString(), altDetail.getValue()); values.put(Field.ALTFIELDTYPE.toString(), ContactSummary.AltFieldType.NAME.ordinal()); } else if (!values.containsKey(Field.STATUSTEXT.toString())) { values.put(Field.STATUSTEXT.toString(), altDetail.getValue()); values.put(Field.ALTFIELDTYPE.toString(), ContactSummary.AltFieldType.STATUS.ordinal()); } else { values.put(Field.ALTFIELDTYPE.toString(), ContactSummary.AltFieldType.UNUSED.ordinal()); } if (altDetail.keyType != null) { values.put(Field.ALTDETAILTYPE.toString(), altDetail.keyType.ordinal()); } } /** * Adds contact summary information to the table for a new contact. If the * contact has no name or no status, an alternative detail will be used such * as telephone number or email address. * * @param contact The new contact * @param writableDb Writable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus addContact(Contact contact, SQLiteDatabase writableDb) { if (Settings.ENABLED_DATABASE_TRACE) { DatabaseHelper.trace(true, "ContactSummeryTable.addContact() contactID[" + contact.contactID + "]"); } if (contact.localContactID == null) { LogUtils.logE("ContactSummeryTable.addContact() Invalid parameters"); return ServiceStatus.ERROR_NOT_FOUND; } try { final ContentValues values = new ContentValues(); values.put(Field.LOCALCONTACTID.toString(), contact.localContactID); values.put(Field.NATIVEID.toString(), contact.nativeContactId); values.put(Field.FRIENDOFMINE.toString(), contact.friendOfMine); values.put(Field.SYNCTOPHONE.toString(), contact.synctophone); ContactDetail altDetail = findAlternativeNameContactDetail(values, contact.details); updateAltValues(values, altDetail); addToPresenceMap(contact.localContactID); if (writableDb.insertOrThrow(TABLE_NAME, null, values) < 0) { LogUtils.logE("ContactSummeryTable.addContact() " + "Unable to insert new contact summary"); return ServiceStatus.ERROR_NOT_FOUND; } return ServiceStatus.SUCCESS; } catch (SQLException e) { LogUtils.logE("ContactSummeryTable.addContact() SQLException - " + "Unable to insert new contact summary", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } } /** * This method returns the most preferred contact detail to be displayed * instead of the contact name when vcard.name is missing. * * @param values - ContentValues to be stored in the DB for the added * contact * @param details - the list of all contact details for the contact being * added * @return the contact detail most suitable to replace the missing * vcard.name. "Value" field may be empty if no suitable contact * detail was found. */ private static ContactDetail findAlternativeNameContactDetail(ContentValues values, List<ContactDetail> details) { ContactDetail altDetail = new ContactDetail(); for (ContactDetail detail : details) { getContactValuesFromDetail(values, detail); if (isPreferredAltDetail(detail, altDetail)) { altDetail.copy(detail); } } return altDetail; } /** * Deletes a contact summary record * * @param localContactID The primary key ID of the contact to delete * @param writableDb Writeable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus deleteContact(Long localContactId, SQLiteDatabase writableDb) { if (Settings.ENABLED_DATABASE_TRACE) { DatabaseHelper.trace(true, "ContactSummeryTable.deleteContact() localContactId[" + localContactId + "]"); } if (localContactId == null) { LogUtils.logE("ContactSummeryTable.deleteContact() Invalid parameters"); return ServiceStatus.ERROR_NOT_FOUND; } try { if (writableDb.delete(TABLE_NAME, Field.LOCALCONTACTID + "=" + localContactId, null) <= 0) { LogUtils.logE("ContactSummeryTable.deleteContact() " + "Unable to delete contact summary"); return ServiceStatus.ERROR_NOT_FOUND; } deleteFromPresenceMap(localContactId); return ServiceStatus.SUCCESS; } catch (SQLException e) { LogUtils.logE("ContactSummeryTable.deleteContact() SQLException - " + "Unable to delete contact summary", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } } /** * Modifies contact parameters. Called when fields in the Contacts table * have been changed. * * @param contact The modified contact * @param writableDb Writable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus modifyContact(Contact contact, SQLiteDatabase writableDb) { if (Settings.ENABLED_DATABASE_TRACE) { DatabaseHelper.trace(true, "ContactSummeryTable.modifyContact() contactID[" + contact.contactID + "]"); } if (contact.localContactID == null) { LogUtils.logE("ContactSummeryTable.modifyContact() Invalid parameters"); return ServiceStatus.ERROR_NOT_FOUND; } try { final ContentValues values = new ContentValues(); values.put(Field.NATIVEID.toString(), contact.nativeContactId); values.put(Field.FRIENDOFMINE.toString(), contact.friendOfMine); values.put(Field.SYNCTOPHONE.toString(), contact.synctophone); String[] args = { contact.localContactID.toString() }; if (writableDb.update(TABLE_NAME, values, Field.LOCALCONTACTID + "=?", args) < 0) { LogUtils.logE("ContactSummeryTable.modifyContact() " + "Unable to update contact summary"); return ServiceStatus.ERROR_NOT_FOUND; } return ServiceStatus.SUCCESS; } catch (SQLException e) { LogUtils.logE("ContactSummeryTable.modifyContact() " + "SQLException - Unable to update contact summary", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } } /** * Adds suitable entries to a ContentValues objects for inserting or * updating the contact summary table, from a contact detail. * * @param contactValues The content values object to update * @param newDetail The new or modified detail * @return true if the summary table has been updated, false otherwise */ private static boolean getContactValuesFromDetail(ContentValues contactValues, ContactDetail newDetail) { switch (newDetail.key) { case VCARD_NAME: if (newDetail.value != null) { VCardHelper.Name name = newDetail.getName(); if (name != null) { String nameStr = name.toString(); // this is what we do to display names of contacts // coming from server if (nameStr.length() > 0) { String nameToLower = name.toString().toLowerCase(); contactValues.put(Field.DISPLAYNAME.toString(), name.toString()); //The value for FIELD.SEARCHNAME is same as Field.DISPLAYNAME but in lowercase. contactValues.put(Field.SEARCHNAME.toString(), nameToLower); } } } return true; // case PRESENCE_TEXT: // if (newDetail.value != null && newDetail.value.length() > 0) { // contactValues.put(Field.STATUSTEXT.toString(), newDetail.value); // contactValues.put(Field.SNS.toString(), newDetail.alt); // } // return true; case PHOTO: if (newDetail.value == null) { contactValues.put(Field.PICTURELOADED.toString(), (Boolean)null); } else { contactValues.put(Field.PICTURELOADED.toString(), false); } return true; default: // Do Nothing. } return false; } /** * Determines if a contact detail should be used in preference to the * current alternative detail (the alternative detail is one that is shown * when a contact has no name or no status). * * @param newDetail The new detail * @param currentDetail The current alternative detail * @return true if the new detail should be used, false otherwise */ private static boolean isPreferredAltDetail(ContactDetail newDetail, ContactDetail currentDetail) { // this means we'll update the detail if (currentDetail.key == null || (currentDetail.key == DetailKeys.UNKNOWN)) { return true; } switch (newDetail.key) { case VCARD_PHONE: // AA:EMAIL,IMADDRESS,ORG will not be updated, PHONE will // consider "preferred" detail check switch (currentDetail.key) { case VCARD_EMAIL: case VCARD_IMADDRESS: case VCARD_ORG: case VCARD_ADDRESS: case VCARD_BUSINESS: case VCARD_TITLE: case VCARD_ROLE: return false; case VCARD_PHONE: break; default: return true; } break; case VCARD_IMADDRESS: // AA:will be updating everything, except for EMAIL and ORG, and // IMADDRESS, when preferred details needs to be considered // first switch (currentDetail.key) { case VCARD_IMADDRESS: break; case VCARD_EMAIL: case VCARD_ORG: case VCARD_ROLE: case VCARD_TITLE: return false; default: return true; } break; case VCARD_ADDRESS: case VCARD_BUSINESS: // AA:will be updating everything, except for EMAIL and ORG, // when preferred details needs to be considered first switch (currentDetail.key) { case VCARD_EMAIL: case VCARD_ORG: case VCARD_ROLE: case VCARD_TITLE: return false; case VCARD_ADDRESS: case VCARD_BUSINESS: break; default: return true; } break; case VCARD_ROLE: case VCARD_TITLE: // AA:will be updating everything, except for EMAIL and ORG, // when preferred details needs to be considered first switch (currentDetail.key) { case VCARD_EMAIL: case VCARD_ORG: return false; case VCARD_ROLE: case VCARD_TITLE: break; default: return true; } break; case VCARD_ORG: // AA:will be updating everything, except for EMAIL and ORG, // when preferred details needs to be considered first switch (currentDetail.key) { case VCARD_EMAIL: return false; case VCARD_ORG: break; default: return true; } break; case VCARD_EMAIL: // AA:will be updating everything, except for EMAIL, when // preferred details needs to be considered first switch (currentDetail.key) { case VCARD_EMAIL: break; default: return true; } break; default: return false; } if (currentDetail.order == null) { return true; } if (newDetail.order != null && newDetail.order.compareTo(currentDetail.order) < 0) { return true; } return false; } /** * Fetches a list of native contact IDs from the summary table (in ascending * order) * * @param summaryList A list that will be populated by this function * @param readableDb Readable SQLite database * @return true if successful, false otherwise */ public static boolean fetchNativeContactIdList(List<Integer> summaryList, SQLiteDatabase readableDb) { if (Settings.ENABLED_DATABASE_TRACE) { DatabaseHelper.trace(false, "ContactSummeryTable.fetchNativeContactIdList()"); } summaryList.clear(); Cursor c = null; try { c = readableDb.rawQuery("SELECT " + Field.NATIVEID + " FROM " + TABLE_NAME + " WHERE " + Field.NATIVEID + " IS NOT NULL" + " ORDER BY " + Field.NATIVEID, null); while (c.moveToNext()) { if (!c.isNull(0)) { summaryList.add(c.getInt(0)); } } return true; } catch (SQLException e) { return false; } finally { CloseUtils.close(c); c = null; } } /** * Modifies the avatar loaded flag for a particular contact * * @param localContactID The primary key ID of the contact * @param value Can be one of the following values: * <ul> * <li>true - The avatar has been loaded</li> * <li>false - There contact has an avatar but it has not yet * been loaded</li> * <li>null - The contact does not have an avatar</li> * </ul> * @param writeableDb Writable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus modifyPictureLoadedFlag(Long localContactId, Boolean value, SQLiteDatabase writeableDb) { if (Settings.ENABLED_DATABASE_TRACE) { DatabaseHelper.trace(true, "ContactSummeryTable.modifyPictureLoadedFlag() localContactId[" + localContactId + "] value[" + value + "]"); } try { ContentValues cv = new ContentValues(); cv.put(Field.PICTURELOADED.toString(), value); String[] args = { String.format("%d", localContactId) }; if (writeableDb.update(TABLE_NAME, cv, Field.LOCALCONTACTID + "=?", args) <= 0) { LogUtils.logE("ContactSummeryTable.modifyPictureLoadedFlag() " + "Unable to modify picture loaded flag"); return ServiceStatus.ERROR_NOT_FOUND; } } catch (SQLException e) { LogUtils.logE("ContactSummeryTable.modifyPictureLoadedFlag() " + "SQLException - Unable to modify picture loaded flag", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } return ServiceStatus.SUCCESS; } /** * Get a group constraint for SQL query depending on the group type. * * @param groupFilterId the group id * @return a String containing the corresponding group constraint */ private static String getGroupConstraint(Long groupFilterId) { if ((groupFilterId != null) && (groupFilterId == GroupsTable.GROUP_PHONEBOOK)) { return " WHERE " + ContactSummaryTable.Field.SYNCTOPHONE + "=" + "1"; } if ((groupFilterId != null) && (groupFilterId == GroupsTable.GROUP_CONNECTED_FRIENDS)) { return " WHERE " + ContactSummaryTable.Field.FRIENDOFMINE + "=" + "1"; } if ((groupFilterId != null) && (groupFilterId == GroupsTable.GROUP_ONLINE)) { return " WHERE " + ContactSummaryTable.Field.LOCALCONTACTID + " IN " + getOnlineWhereClause(); } return " INNER JOIN " + ContactGroupsTable.TABLE_NAME + " WHERE " + ContactSummaryTable.TABLE_NAME + "." + ContactSummaryTable.Field.LOCALCONTACTID + "=" + ContactGroupsTable.TABLE_NAME + "." + ContactGroupsTable.Field.LOCALCONTACTID + " AND " + ContactGroupsTable.Field.ZYBGROUPID + "=" + groupFilterId; } /** * Fetches a contact list cursor for a given filter and search constraint * * @param groupFilterId The server group ID or null to fetch all groups * @param constraint A search string or null to fetch without constraint * @param meProfileId The current me profile Id which should be excluded * from the returned list. * @param readableDb Readable SQLite database * @return The cursor or null if an error occurred * @see #getQueryData(Cursor) */ public static Cursor openContactSummaryCursor(Long groupFilterId, CharSequence constraint, Long meProfileId, SQLiteDatabase readableDb) { if (Settings.ENABLED_DATABASE_TRACE) { DatabaseHelper.trace(false, "ContactSummeryTable.fetchContactList() " + "groupFilterId[" + groupFilterId + "] constraint[" + constraint + "]" + " meProfileId[" + meProfileId + "]"); } try { if (meProfileId == null) { // Ensure that when the profile is not available the function doesn't fail // Since "Field <> null" always returns false meProfileId = -1L; } final StringBuilder queryString = new StringBuilder("SELECT ").append(getFullQueryList()) .append(" FROM ").append(TABLE_NAME); // Add group constraint if any if (groupFilterId == null) { queryString.append(" WHERE "); } else { queryString.append(getGroupConstraint(groupFilterId)).append(" AND "); } // Check if this is a search request /** * Rather than checking the searchConstraint with the Field.DISPLAYNAME, we are comparing it with Field.SEARCHNAME * so that it can handle the comparison of special characters like à, è, ù, â, ê, î, ô CASE insensitively. */ if (constraint != null) { final String dbSafeConstraint = DatabaseUtils.sqlEscapeString("%" + constraint.toString().toLowerCase() + "%"); queryString.append(Field.SEARCHNAME).append(" LIKE ").append(dbSafeConstraint).append(" AND "); } queryString.append(TABLE_NAME).append(".") .append(Field.LOCALCONTACTID).append("!=").append(meProfileId) .append(" ORDER BY LOWER(").append(Field.DISPLAYNAME).append(")"); // Sort results using localized collate method queryString.append(LOCALIZED_COLLATE); return readableDb.rawQuery(queryString.toString(), null); } catch (SQLException e) { LogUtils.logE("ContactSummeryTable.fetchContactList() " + "SQLException - Unable to fetch filtered summary cursor", e); return null; } } /** * Fetches an SQLite statement object which can be used to merge the native * information from one contact to another. * * @param writableDb Writable SQLite database * @return The SQL statement, or null if a compile error occurred * @see #mergeContact(ContactIdInfo, SQLiteStatement) */ public static SQLiteStatement mergeContactStatement(SQLiteDatabase writableDb) { if (Settings.ENABLED_DATABASE_TRACE) { DatabaseHelper.trace(true, "ContactSummeryTable.mergeContact()"); } try { return writableDb.compileStatement(UPDATE_NATIVE_ID_BY_LOCAL_CONTACT_ID); } catch (SQLException e) { LogUtils.logE("ContactSummaryTable.mergeContactStatement() compile error:\n", e); return null; } } /** * Copies the contact native information from one contact to another * * @param info Copies the {@link ContactIdInfo#nativeId} value to the * contact with local ID {@link ContactIdInfo#mergedLocalId}. * @param statement The statement returned by * {@link #mergeContactStatement(SQLiteDatabase)}. * @return SUCCESS or a suitable error code * @see #mergeContactStatement(SQLiteDatabase) */ public static ServiceStatus mergeContact(ContactIdInfo info, SQLiteStatement statement) { if (Settings.ENABLED_DATABASE_TRACE) { DatabaseHelper.trace(true, "ContactSummeryTable.mergeContact()"); } if (statement == null) { return ServiceStatus.ERROR_DATABASE_CORRUPT; } try { if (info.nativeId == null) statement.bindNull(1); else statement.bindLong(1, info.nativeId); statement.bindLong(2, info.mergedLocalId); statement.execute(); return ServiceStatus.SUCCESS; } catch (SQLException e) { LogUtils.logE("ContactSummeryTable.mergeContact() " + "SQLException - Unable to merge contact summary native info:\n", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } } /** * TODO: be careful * * @param user * @param writableDb * @return */ public synchronized static ServiceStatus updateOnlineStatus(User user) { sPresenceMap.put(user.getLocalContactId(), user.isOnline()); return ServiceStatus.SUCCESS; } /** * This method sets users offline except for provided local contact ids. * @param userIds - ArrayList of integer user ids, if null - all user will be removed from the presence hash. * @param writableDb - database. */ public synchronized static void setUsersOffline(ArrayList<Long> userIds) { Iterator<Long> itr = sPresenceMap.keySet().iterator(); Long localId = null; while(itr.hasNext()) { localId = itr.next(); if (userIds == null || !userIds.contains(localId)) { itr.remove(); } } } /** * @param user * @param writableDb * @return */ public synchronized static ServiceStatus setOfflineStatus() { // If any contact is not present within the presenceMap, then its status // is considered as OFFLINE. This is taken care in the getPresence API. if (sPresenceMap != null) { sPresenceMap.clear(); } return ServiceStatus.SUCCESS; } /** * @param localContactIdOfMe * @param writableDb * @return */ public synchronized static ServiceStatus setOfflineStatusExceptForMe(long localContactIdOfMe) { // If any contact is not present within the presenceMap, then its status // is considered as OFFLINE. This is taken care in the getPresence API. if (sPresenceMap != null) { sPresenceMap.clear(); sPresenceMap.put(localContactIdOfMe, OnlineStatus.OFFLINE.ordinal()); } return ServiceStatus.SUCCESS; } /** * Updates the native IDs for a list of contacts. * * @param contactIdList A list of ContactIdInfo objects. For each object, * the local ID must match a local contact ID in the table. The * Native ID will be used for the update. Other fields are * unused. * @param writeableDb Writable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus syncSetNativeIds(List<ContactIdInfo> contactIdList, SQLiteDatabase writableDb) { DatabaseHelper.trace(true, "ContactSummaryTable.syncSetNativeIds()"); if (contactIdList.size() == 0) { return ServiceStatus.SUCCESS; } final SQLiteStatement statement1 = writableDb.compileStatement("UPDATE " + TABLE_NAME + " SET " + Field.NATIVEID + "=? WHERE " + Field.LOCALCONTACTID + "=?"); for (int i = 0; i < contactIdList.size(); i++) { final ContactIdInfo info = contactIdList.get(i); try { writableDb.beginTransaction(); if (info.nativeId == null) { statement1.bindNull(1); } else { statement1.bindLong(1, info.nativeId); } statement1.bindLong(2, info.localId); statement1.execute(); writableDb.setTransactionSuccessful(); } catch (SQLException e) { LogUtils.logE("ContactSummaryTable.syncSetNativeIds() " + "SQLException - Unable to update contact native Ids", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } finally { writableDb.endTransaction(); } } return ServiceStatus.SUCCESS; } /** * Updates the summary for a contact Replaces the complex logic of updating * the summary with a new contactdetail. Instead the method gets a whole * contact after it has been modified and builds the summary infos. * * @param contact A Contact object that has been modified * @param writeableDb Writable SQLite database * @param isMeProfile Specifies if the contact in question is the Me Contact or not * @return String - the contact name to display and null in case of database error. */ public static String updateContactDisplayName(Contact contact, SQLiteDatabase writableDb, boolean isMeProfile) { ContactDetail name = getDisplayNameDetail(contact); String nameString = null; if(isVcardNameDetail(name)) { nameString = name.getName().toString(); } else if(!isMeProfile) { if(name != null) { // Apply non VCard name nameString = name.getValue(); } if (nameString == null) { // Unknown name nameString = ContactDetail.UNKNOWN_NAME; } } else { // Me Profile with no name - set the default name nameString = SyncMeDbUtils.ME_PROFILE_DEFAULT_NAME; } // Start updating the table SQLiteStatement statement = null; try { final StringBuffer updateQuery = StringBufferPool.getStringBuffer(SQLKeys.UPDATE); updateQuery.append(TABLE_NAME).append(SQLKeys.SET).append(Field.DISPLAYNAME). append("=?, ").append(Field.SEARCHNAME).append("=?").append(" WHERE ").append(Field.LOCALCONTACTID).append("=?"); statement = writableDb.compileStatement(StringBufferPool.toStringThenRelease(updateQuery)); writableDb.beginTransaction(); statement.bindString(1, nameString); //need to update the Field.SEARCHNAME too. statement.bindString(2, nameString.toLowerCase()); statement.bindLong(3, contact.localContactID); statement.execute(); writableDb.setTransactionSuccessful(); } catch (SQLException e) { LogUtils.logE("ContactSummaryTable.updateNameAndStatus() " + "SQLException - Unable to update contact native Ids", e); return null; } finally { writableDb.endTransaction(); if (statement != null) { statement.close(); statement = null; } } return nameString; } /** * Utility method to determine if a Detail is of the VCard Name key type * @param detail The Contact Detail to check * @return true if not null and of the VCARD_NAME key type, false if not */ private static boolean isVcardNameDetail(ContactDetail detail) { return detail != null && detail.key == ContactDetail.DetailKeys.VCARD_NAME; } /** * Retrieves display name detail for a contact * @param contact Contact to retrieve display name from * @return Found display name detail - maybe be null */ private static ContactDetail getDisplayNameDetail(Contact contact) { // These two Arrays contains the order in which the details are queried. // First valid (not empty or unknown) detail is taken ContactDetail.DetailKeys preferredNameDetails[] = { ContactDetail.DetailKeys.VCARD_NAME, ContactDetail.DetailKeys.VCARD_ORG, ContactDetail.DetailKeys.VCARD_EMAIL, ContactDetail.DetailKeys.VCARD_PHONE }; ContactDetail name = null; // Query the details for the name field for (ContactDetail.DetailKeys key : preferredNameDetails) { if ((name = contact.getContactDetail(key)) != null) { // Some contacts have only email but the name detail!=null // (gmail for example) if (key == ContactDetail.DetailKeys.VCARD_NAME && name.getName() == null) continue; if (key != ContactDetail.DetailKeys.VCARD_NAME && TextUtils.isEmpty(name.getValue())) continue; break; } } return name; } /** * LocalId = ? */ private final static String SQL_STRING_LOCAL_ID_EQUAL_QUESTION_MARK = Field.LOCALCONTACTID + " = ?"; /** * * @param localContactId * @param writableDb * @return */ public static boolean setNativeContactId(long localContactId, long nativeContactId, SQLiteDatabase writableDb) { final ContentValues values = new ContentValues(); values.put(Field.NATIVEID.toString(), nativeContactId); try { if (writableDb.update(TABLE_NAME, values, SQL_STRING_LOCAL_ID_EQUAL_QUESTION_MARK, new String[] { Long.toString(localContactId) }) == 1) { return true; } } catch (Exception e) { LogUtils.logE("ContactsTable.setNativeContactId() Exception - " + e); } return false; } /** * Clears the Presence Map table. This needs to be called whenever the ContactSummaryTable is cleared * or recreated. */ private synchronized static void clearPresenceMap() { sPresenceMap.clear(); } /** * Fetches the presence of the contact with localContactID * * @param localContactID * @return the presence status of the contact */ public synchronized static OnlineStatus getPresence(Long localContactID) { OnlineStatus onlineStatus = OnlineStatus.OFFLINE; Integer val = sPresenceMap.get(localContactID); if (val != null) { if (val < ContactSummary.OnlineStatus.values().length) { onlineStatus = ContactSummary.OnlineStatus.values()[val]; } } return onlineStatus; } /** * This API should be called whenever a contact is added. The presenceMap should be consistent * with the ContactSummaryTable. Hence the default status of OFFLINE is set for every contact added * @param localContactID */ private synchronized static void addToPresenceMap(Long localContactID) { sPresenceMap.put(localContactID, OnlineStatus.OFFLINE.ordinal()); } /** * This API should be called whenever a contact is deleted from teh ContactSUmmaryTable. This API * removes the presence information for the given contact * @param localContactId */ private synchronized static void deleteFromPresenceMap(Long localContactId) { sPresenceMap.remove(localContactId); } /** * This API creates the string to be used in the IN clause when getting the list of all * online contacts. * @return The list of contacts in the proper format for the IN list */ private synchronized static String getOnlineWhereClause() { Set<Entry<Long, Integer>> set = sPresenceMap.entrySet(); Iterator<Entry<Long, Integer>> i = set.iterator(); StringBuilder inClause = new StringBuilder("("); boolean isFirst = true; while (i.hasNext()) { Entry<Long, Integer> me = (Entry<Long, Integer>) i.next(); Integer value = me.getValue(); if (value != null && (value == OnlineStatus.ONLINE.ordinal() || value == OnlineStatus.IDLE .ordinal())) { if (isFirst == false) { inClause.append(','); } else { isFirst = false; } inClause.append(me.getKey()); } } if (isFirst) { return "()"; } inClause.append(')'); return inClause.toString(); } /** * Fetches the formattedName for the corresponding localContactId. * * @param localContactId The primary key ID of the contact to find * @param readableDb Readable SQLite database * @return String formattedName or NULL on error */ public static String fetchFormattedNamefromLocalContactId( final long localContactId, final SQLiteDatabase readableDb) { if (Settings.ENABLED_DATABASE_TRACE) { DatabaseHelper.trace(false, "ContactSummaryTable.fetchFormattedNamefromLocalContactId" + " localContactId[" + localContactId + "]"); } Cursor c1 = null; String formattedName = null; try { String query = "SELECT " + Field.DISPLAYNAME + " FROM " + TABLE_NAME + " WHERE " + Field.LOCALCONTACTID + "=" + localContactId; c1 = readableDb.rawQuery(query, null); if (c1 != null && c1.getCount() > 0) { c1.moveToFirst(); formattedName = c1.getString(0); } return formattedName; } catch (SQLiteException e) { LogUtils .logE( "fetchFormattedNamefromLocalContactId() " + "Exception - Unable to fetch contact summary", e); return formattedName; } finally { CloseUtils.close(c1); c1 = null; } } }