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