/* * CDDL HEADER START * * The contents of this file are subject to the terms of the Common Development * and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at * src/com/vodafone360/people/VODAFONE.LICENSE.txt or * http://github.com/360/360-Engine-for-Android * See the License for the specific language governing permissions and * limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each file and * include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the fields * enclosed by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved. * Use is subject to license terms. */ package com.vodafone360.people.database.tables; import java.util.ArrayList; import java.util.List; import android.content.ContentValues; import android.content.Context; 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.net.Uri; import android.provider.CallLog.Calls; import android.telephony.PhoneNumberUtils; 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.utils.SqlUtils; import com.vodafone360.people.datatypes.ActivityContact; import com.vodafone360.people.datatypes.ActivityItem; import com.vodafone360.people.datatypes.ContactSummary; import com.vodafone360.people.service.ServiceStatus; import com.vodafone360.people.utils.CloseUtils; import com.vodafone360.people.utils.LogUtils; import com.vodafone360.people.utils.StringBufferPool; import com.vodafone360.people.utils.WidgetUtils; /** * Contains all the functionality related to the activities database table. This * class is never instantiated hence all methods must be static. * * @version %I%, %G% */ public abstract class ActivitiesTable { /*** * The name of the table as it appears in the database. */ public static final String TABLE_NAME = "Activities"; private static final String TABLE_INDEX_NAME = "ActivitiesIndex"; /** Database cleanup will delete any activity older than X days. **/ private static final int CLEANUP_MAX_AGE_DAYS = 20; /** Database cleanup will delete older activities after the first X. **/ private static final int CLEANUP_MAX_QUANTITY = 400; /** * Flag that indicates the most recent activity for all activities of the contact. * See {@link ActivitiesTable.Field#LATEST_CONTACT_STATUS} for more details. **/ private static final int LATEST_STATUS_FOR_ALL = 0x01; /** * Flag that indicates the most recent activity for a specific native item type * of the contact. * See {@link ActivitiesTable.Field#LATEST_CONTACT_STATUS} for more details. **/ private static final int LATEST_STATUS_FOR_TYPE = 0x02; /*** * An enumeration of all the field names in the database. */ public static enum Field { /** Local timeline id. **/ LOCAL_ACTIVITY_ID("LocalId"), /** Activity ID. **/ ACTIVITY_ID("activityid"), /** Timestamp. */ TIMESTAMP("time"), /** Type of the event. **/ TYPE("type"), /** URI. */ URI("uri"), /** Title for timelines . **/ TITLE("title"), /** Contents of timelines/statuses. **/ DESCRIPTION("description"), /** Preview URL. **/ PREVIEW_URL("previewurl"), /** Store. **/ STORE("store"), /** Type of the event: status, chat messages, phone call or SMS/MMS. **/ FLAG("flag"), /** Parent Activity. **/ PARENT_ACTIVITY("parentactivity"), /** Has children. **/ HAS_CHILDREN("haschildren"), /** Visibility. **/ VISIBILITY("visibility"), /** More info. **/ MORE_INFO("moreinfo"), /** Contact ID. **/ CONTACT_ID("contactid"), /** User ID. **/ USER_ID("userid"), /** Contact name or the alternative. **/ CONTACT_NAME("contactname"), /** Other contact's localContactId. **/ LOCAL_CONTACT_ID("contactlocalid"), /** @see SocialNetwork. **/ CONTACT_NETWORK("contactnetwork"), /** Contact address. **/ CONTACT_ADDRESS("contactaddress"), /** Contact avatar URL. **/ CONTACT_AVATAR_URL("contactavatarurl"), /** Native item type. **/ NATIVE_ITEM_TYPE("nativeitemtype"), /** Native item ID. **/ NATIVE_ITEM_ID("nativeitemid"), /** * Latest contact status. * This field is used as a bitfield to indicate the most recent activities. * Currently we use the following two flags: * <ul> * <li> * {@link ActivitiesTable#LATEST_STATUS_FOR_ALL} indicates the most recent activity * for the contact associated with this database entry. Therefore only one activity * per contact should have <code>LATEST_STATUS_FOR_ALL</code> set. * </li><li> * {@link ActivitiesTable#LATEST_STATUS_FOR_TYPE} indicates the most recent activity * for a native item type for the contact associated with this database entry. A contact * with several different activity types (e.g. call and text message) will have * <code>LATEST_STATUS_FOR_TYPE</code> set for all of its most recent activities if the * type differs to already set activities. Former activities of the same native item * type should not set <code>LATEST_STATUS_FOR_TYPE</code> (or * <code>LATEST_STATUS_FOR_ALL</code>). * </li> * </ul> * The above applies only to timeline related activities. For status related * activities only <code>LATEST_STATUS_FOR_ALL</code> is set to indicate the most recent * status entry. <code>LATEST_STATUS_FOR_TYPEL</code> is omitted. **/ LATEST_CONTACT_STATUS("latestcontactstatus"), /** Native thread ID. **/ NATIVE_THREAD_ID("nativethreadid"), /** For chat messages: if this message is incoming. **/ INCOMING("incoming"); /** 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(final String field) { mField = field; } /** * @return the name of the field as it appears in the database. */ public String toString() { return mField; } } /** * An enumeration of supported timeline types. */ public static enum TimelineNativeTypes { /** Call log type. **/ CallLog, /** SMS log type. **/ SmsLog, /** MMS log type. **/ MmsLog, /** Chat log type. **/ ChatLog } /** * This class encapsulates a timeline activity item. */ public static class TimelineSummaryItem { /*** * Enum of Timeline types. */ public enum Type { /** Incoming type. **/ INCOMING, /** Outgoing type. **/ OUTGOING, /** Unsent type. **/ UNSENT, /** Unknown type (do not use). **/ UNKNOWN } /*** * Get the Type from a given Integer value. * @param input Integer.ordinal value of the Type * @return Relevant Type or UNKNOWN if the Integer is not known. */ public static Type getType(final int input) { if (input < 0 || input > Type.UNKNOWN.ordinal()) { return Type.UNKNOWN; } else { return Type.values()[input]; } } /** Maps to the local activity ID (primary key). **/ private Long mLocalActivityId; /** Maps to the activity timestamp in the table. **/ public Long mTimestamp; /** Maps to the contact name in the table. **/ public String mContactName; /** Set to true if there is an avatar URL stored in the table. **/ private boolean mHasAvatar; /** Maps to type in the table. **/ public ActivityItem.Type mType; /** Maps to local contact id in the table. **/ public Long mLocalContactId; /** Maps to contact network stored in the table. **/ public String mContactNetwork; /** Maps to title stored in the table. **/ public String mTitle; /** Maps to description stored in the table. **/ public String mDescription; /** * Maps to native item type in the table Can be an ordinal from the * {@link ActivitiesTable#TimelineNativeTypes}. */ public Integer mNativeItemType; /** * Key linking to the call-log or message-log item in the native * database. */ public Integer mNativeItemId; /** Server contact ID. **/ public Long mContactId; /** User ID from the server. **/ public Long mUserId; /** Thread ID from the native database (for messages). **/ public Integer mNativeThreadId; /** Contact address (phone number or email address). **/ public String mContactAddress; /** Messages can be incoming and outgoing. **/ public Type mIncoming; /** * Returns a string describing the timeline summary item. * * @return String describing the timeline summary item. */ @Override public final String toString() { final StringBuilder sb = new StringBuilder("TimeLineSummaryItem [mLocalActivityId["); sb.append(mLocalActivityId); sb.append("], mTimestamp["); sb.append(mTimestamp); sb.append("], mContactName["); sb.append(mContactName); sb.append("], mHasAvatar["); sb.append(mHasAvatar); sb.append("], mType["); sb.append(mType); sb.append("], mLocalContactId["); sb.append(mLocalContactId); sb.append("], mContactNetwork["); sb.append(mContactNetwork); sb.append("], mTitle["); sb.append(mTitle); sb.append("], mDescription["); sb.append(mDescription); sb.append("], mNativeItemType["); sb.append(mNativeItemType); sb.append("], mNativeItemId["); sb.append(mNativeItemId); sb.append("], mContactId["); sb.append(mContactId); sb.append("], mUserId["); sb.append(mUserId); sb.append("], mNativeThreadId["); sb.append(mNativeThreadId); sb.append("], mContactAddress["); sb.append(mContactAddress); sb.append("], mIncoming["); sb.append(mIncoming); sb.append("]]");; return sb.toString(); } @Override public final boolean equals(final Object object) { if (TimelineSummaryItem.class != object.getClass()) { return false; } TimelineSummaryItem item = (TimelineSummaryItem) object; return mLocalActivityId.equals(item.mLocalActivityId) && mTimestamp.equals(item.mTimestamp) && mContactName.equals(item.mContactName) && mHasAvatar == item.mHasAvatar && mType.equals(item.mType) && mLocalContactId.equals(item.mLocalContactId) && mContactNetwork.equals(item.mContactNetwork) && mTitle.equals(item.mTitle) && mDescription.equals(item.mDescription) && mNativeItemType.equals(item.mNativeItemType) && mNativeItemId.equals(item.mNativeItemId) && mContactId.equals(item.mContactId) && mUserId.equals(item.mUserId) && mNativeThreadId.equals(item.mNativeThreadId) && mContactAddress.equals(item.mContactAddress) && mIncoming.equals(item.mIncoming); } }; /** Number of milliseconds in a day. **/ private static final int NUMBER_OF_MS_IN_A_DAY = 24 * 60 * 60 * 1000; /** Number of milliseconds in a second. **/ private static final int NUMBER_OF_MS_IN_A_SECOND = 1000; /*** * Private constructor to prevent instantiation. */ private ActivitiesTable() { // Do nothing. } /** * Create Activities Table. * * @param writeableDb A writable SQLite database. */ public static void create(final SQLiteDatabase writeableDb) { DatabaseHelper.trace(true, "DatabaseHelper.create()"); writeableDb.execSQL("CREATE TABLE " + TABLE_NAME + " (" + Field.LOCAL_ACTIVITY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + Field.ACTIVITY_ID + " LONG, " + Field.TIMESTAMP + " LONG, " + Field.TYPE + " TEXT, " + Field.URI + " TEXT, " + Field.TITLE + " TEXT, " + Field.DESCRIPTION + " TEXT, " + Field.PREVIEW_URL + " TEXT, " + Field.STORE + " TEXT, " + Field.FLAG + " INTEGER, " + Field.PARENT_ACTIVITY + " LONG, " + Field.HAS_CHILDREN + " INTEGER, " + Field.VISIBILITY + " INTEGER, " + Field.MORE_INFO + " TEXT, " + Field.CONTACT_ID + " LONG, " + Field.USER_ID + " LONG, " + Field.CONTACT_NAME + " TEXT, " + Field.LOCAL_CONTACT_ID + " LONG, " + Field.CONTACT_NETWORK + " TEXT, " + Field.CONTACT_ADDRESS + " TEXT, " + Field.CONTACT_AVATAR_URL + " TEXT, " + Field.LATEST_CONTACT_STATUS + " INTEGER, " + Field.NATIVE_ITEM_TYPE + " INTEGER, " + Field.NATIVE_ITEM_ID + " INTEGER, " + Field.NATIVE_THREAD_ID + " INTEGER, " + Field.INCOMING + " INTEGER);"); writeableDb.execSQL("CREATE INDEX " + TABLE_INDEX_NAME + " ON " + TABLE_NAME + " ( " + Field.TIMESTAMP + " )"); } /** * Fetches a comma separated list of table fields which can be used in an * SQL SELECT statement as the query projection. One of the * {@link #getQueryData} methods can used to fetch data from the cursor. * * @return SQL string */ private static String getFullQueryList() { DatabaseHelper.trace(false, "DatabaseHelper.getFullQueryList()"); final StringBuffer fullQuery = StringBufferPool.getStringBuffer(); fullQuery.append(Field.LOCAL_ACTIVITY_ID).append(SqlUtils.COMMA). append(Field.ACTIVITY_ID).append(SqlUtils.COMMA). append(Field.TIMESTAMP).append(SqlUtils.COMMA). append(Field.TYPE).append(SqlUtils.COMMA). append(Field.URI).append(SqlUtils.COMMA). append(Field.TITLE).append(SqlUtils.COMMA). append(Field.DESCRIPTION).append(SqlUtils.COMMA). append(Field.PREVIEW_URL).append(SqlUtils.COMMA). append(Field.STORE).append(SqlUtils.COMMA). append(Field.FLAG).append(SqlUtils.COMMA). append(Field.PARENT_ACTIVITY).append(SqlUtils.COMMA). append(Field.HAS_CHILDREN).append(SqlUtils.COMMA). append(Field.VISIBILITY).append(SqlUtils.COMMA). append(Field.MORE_INFO).append(SqlUtils.COMMA). append(Field.CONTACT_ID).append(SqlUtils.COMMA). append(Field.USER_ID).append(SqlUtils.COMMA). append(Field.CONTACT_NAME).append(SqlUtils.COMMA). append(Field.LOCAL_CONTACT_ID).append(SqlUtils.COMMA). append(Field.CONTACT_NETWORK).append(SqlUtils.COMMA). append(Field.CONTACT_ADDRESS).append(SqlUtils.COMMA). append(Field.CONTACT_AVATAR_URL).append(SqlUtils.COMMA). append(Field.INCOMING); return StringBufferPool.toStringThenRelease(fullQuery); } /** * Fetches activities information from a cursor at the current position. The * {@link #getFullQueryList()} method should be used to make the query. * * @param cursor The cursor returned by the query * @param activityItem An empty activity object that will be filled with the * result * @param activityContact An empty activity contact object that will be * filled */ public static void getQueryData(final Cursor cursor, final ActivityItem activityItem, final ActivityContact activityContact) { DatabaseHelper.trace(false, "DatabaseHelper.getQueryData()"); /** Populate ActivityItem. **/ activityItem.localActivityId = SqlUtils.setLong(cursor, Field.LOCAL_ACTIVITY_ID.toString(), null); activityItem.activityId = SqlUtils.setLong(cursor, Field.ACTIVITY_ID.toString(), null); activityItem.time = SqlUtils.setLong(cursor, Field.TIMESTAMP.toString(), null); activityItem.type = SqlUtils.setActivityItemType(cursor, Field.TYPE.toString()); activityItem.uri = SqlUtils.setString(cursor, Field.URI.toString()); activityItem.title = SqlUtils.setString(cursor, Field.TITLE.toString()); activityItem.description = SqlUtils.setString(cursor, Field.DESCRIPTION.toString()); activityItem.previewUrl = SqlUtils.setString(cursor, Field.PREVIEW_URL.toString()); activityItem.store = SqlUtils.setString(cursor, Field.STORE.toString()); activityItem.activityFlags = SqlUtils.setInt(cursor, Field.FLAG.toString(), null); activityItem.parentActivity = SqlUtils.setLong(cursor, Field.PARENT_ACTIVITY.toString(), null); activityItem.hasChildren = SqlUtils.setBoolean(cursor, Field.HAS_CHILDREN.toString(), activityItem.hasChildren); activityItem.visibilityFlags = SqlUtils.setInt(cursor, Field.VISIBILITY.toString(), null); // TODO: Field MORE_INFO is not used, consider deleting. /** Populate ActivityContact. **/ getQueryData(cursor, activityContact); } /** * Fetches activities information from a cursor at the current position. The * {@link #getFullQueryList()} method should be used to make the query. * * @param cursor The cursor returned by the query. * @param activityContact An empty activity contact object that will be * filled */ public static void getQueryData(final Cursor cursor, final ActivityContact activityContact) { DatabaseHelper.trace(false, "DatabaseHelper.getQueryData()"); /** Populate ActivityContact. **/ activityContact.mContactId = SqlUtils.setLong(cursor, Field.CONTACT_ID.toString(), null); activityContact.mUserId = SqlUtils.setLong(cursor, Field.USER_ID.toString(), null); activityContact.mName = SqlUtils.setString(cursor, Field.CONTACT_NAME.toString()); activityContact.mLocalContactId = SqlUtils.setLong(cursor, Field.LOCAL_CONTACT_ID.toString(), null); activityContact.mNetwork = SqlUtils.setString(cursor, Field.CONTACT_NETWORK.toString()); activityContact.mAddress = SqlUtils.setString(cursor, Field.CONTACT_ADDRESS.toString()); activityContact.mAvatarUrl = SqlUtils.setString(cursor, Field.CONTACT_AVATAR_URL.toString()); } /*** * Provides a ContentValues object that can be used to update the table. * * @param item The source activity item * @param contactIdx The index of the contact to use for the update, or null * to exclude contact specific information. * @return ContentValues for use in an SQL update or insert. * @note Items that are NULL will be not modified in the database. */ private static ContentValues fillUpdateData(final ActivityItem item, final Integer contactIdx) { DatabaseHelper.trace(false, "DatabaseHelper.fillUpdateData()"); ContentValues activityItemValues = new ContentValues(); ActivityContact ac = null; if (contactIdx != null) { ac = item.contactList.get(contactIdx); } activityItemValues.put(Field.ACTIVITY_ID.toString(), item.activityId); activityItemValues.put(Field.TIMESTAMP.toString(), item.time); if (item.type != null) { activityItemValues.put(Field.TYPE.toString(), item.type.getTypeCode()); } if (item.uri != null) { activityItemValues.put(Field.URI.toString(), item.uri); } /** TODO: Not sure if we need this. **/ // activityItemValues.put(Field.INCOMING.toString(), false); activityItemValues.put(Field.TITLE.toString(), item.title); activityItemValues.put(Field.DESCRIPTION.toString(), item.description); if (item.previewUrl != null) { activityItemValues.put(Field.PREVIEW_URL.toString(), item.previewUrl); } if (item.store != null) { activityItemValues.put(Field.STORE.toString(), item.store); } if (item.activityFlags != null) { activityItemValues.put(Field.FLAG.toString(), item.activityFlags); } if (item.parentActivity != null) { activityItemValues.put(Field.PARENT_ACTIVITY.toString(), item.parentActivity); } if (item.hasChildren != null) { activityItemValues.put(Field.HAS_CHILDREN.toString(), item.hasChildren); } if (item.visibilityFlags != null) { activityItemValues.put(Field.VISIBILITY.toString(), item.visibilityFlags); } if (ac != null) { activityItemValues.put(Field.CONTACT_ID.toString(), ac.mContactId); activityItemValues.put(Field.USER_ID.toString(), ac.mUserId); activityItemValues.put(Field.CONTACT_NAME.toString(), ac.mName); activityItemValues.put(Field.LOCAL_CONTACT_ID.toString(), ac.mLocalContactId); if (ac.mNetwork != null) { activityItemValues.put(Field.CONTACT_NETWORK.toString(), ac.mNetwork); } if (ac.mAddress != null) { activityItemValues.put(Field.CONTACT_ADDRESS.toString(), ac.mAddress); } if (ac.mAvatarUrl != null) { activityItemValues.put(Field.CONTACT_AVATAR_URL.toString(), ac.mAvatarUrl); } } return activityItemValues; } /** * Fetches a list of status items from the given time stamp. * * @param timeStamp Time stamp in milliseconds * @param readableDb Readable SQLite database * @return A cursor (use one of the {@link #getQueryData} methods to read * the data) */ public static Cursor fetchStatusEventList(final long timeStamp, final SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper.fetchStatusEventList()"); return readableDb.rawQuery("SELECT " + getFullQueryList() + " FROM " + TABLE_NAME + " WHERE (" + Field.FLAG + " & " + ActivityItem.STATUS_ITEM + ") AND " + Field.TIMESTAMP + " > " + timeStamp + " ORDER BY " + Field.TIMESTAMP + " DESC", null); } /** * Returns a list of activity IDs already synced, in reverse chronological * order Fetches from the given timestamp. * * @param actIdList An empty list which will be filled with the result * @param timeStamp The time stamp to start the fetch * @param readableDb Readable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus fetchActivitiesIds(final List<Long> actIdList, final Long timeStamp, final SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper.fetchActivitiesIds()"); Cursor cursor = null; try { long queryTimeStamp; if (timeStamp != null) { queryTimeStamp = timeStamp; } else { queryTimeStamp = 0; } cursor = readableDb.rawQuery("SELECT " + Field.ACTIVITY_ID + " FROM " + TABLE_NAME + " WHERE " + Field.TIMESTAMP + " >= " + queryTimeStamp + " ORDER BY " + Field.TIMESTAMP + " DESC", null); while (cursor.moveToNext()) { actIdList.add(cursor.getLong(0)); } } catch (SQLiteException e) { LogUtils.logE("ActivitiesTable.fetchActivitiesIds()" + "Unable to fetch group list", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } finally { CloseUtils.close(cursor); } return ServiceStatus.SUCCESS; } /** * Adds a list of activities to table. The activities added will be grouped * in the database, based on local contact Id, name or contact address (see * {@link #removeContactGroup(Long, String, Long, int, * TimelineNativeTypes[], SQLiteDatabase)} * for more information on how the grouping works. * * @param actList The list of activities * @param writableDb Writable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus addActivities(final List<ActivityItem> actList, final SQLiteDatabase writableDb, final Context context) { DatabaseHelper.trace(true, "DatabaseHelper.addActivities()"); SQLiteStatement statement = ContactsTable.fetchLocalFromServerIdStatement(writableDb); boolean isMeProfileChanged = false; Long meProfileId = StateTable.fetchMeProfileId(writableDb); for (ActivityItem activity : actList) { try { writableDb.beginTransaction(); if (activity.contactList != null) { int clistSize = activity.contactList.size(); for (int i = 0; i < clistSize; i++) { final ActivityContact activityContact = activity.contactList.get(i); activityContact.mLocalContactId = ContactsTable.fetchLocalFromServerId( activityContact.mContactId, statement); // Check if me profile status has been modified. boolean isMeProfile = meProfileId != null && meProfileId.equals(activityContact.mLocalContactId); // ORing to ensure that the value remains true once set isMeProfileChanged |= isMeProfile; if (activityContact.mLocalContactId == null) { // Just skip activities for which we don't have a corresponding contact // in the database anymore otherwise they will be shown as "Blank name". // This is the same on the web but we could use the provided name instead. continue; } else { // Find a more up-to-date name as the names in the Activities are the ones // from submit time. If they changed in the meantime, this is not reflected // so we fetch the names from the ContactSummary table. final ContactSummary contactSummary = new ContactSummary(); if (ContactSummaryTable.fetchSummaryItem(activityContact.mLocalContactId, contactSummary, writableDb) == ServiceStatus.SUCCESS) { // Me Profile can have empty name if ((isMeProfile && contactSummary.formattedName != null) || !TextUtils.isEmpty(contactSummary.formattedName)) { activityContact.mName = contactSummary.formattedName; } } } int latestStatusVal = removeContactGroup( activityContact.mLocalContactId, activityContact.mName, activity.time, activity.activityFlags, null, writableDb); ContentValues cv = fillUpdateData(activity, i); cv.put(Field.LATEST_CONTACT_STATUS.toString(), latestStatusVal); activity.localActivityId = writableDb.insertOrThrow(TABLE_NAME, null, cv); } } else { activity.localActivityId = writableDb.insertOrThrow( TABLE_NAME, null, fillUpdateData(activity, null)); } if ((activity.localActivityId != null) && (activity.localActivityId < 0)) { LogUtils.logE("ActivitiesTable.addActivities() " + "Unable to add activity"); return ServiceStatus.ERROR_DATABASE_CORRUPT; } writableDb.setTransactionSuccessful(); } catch (SQLException e) { LogUtils.logE("ActivitiesTable.addActivities() " + "Unable to add activity", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } finally { writableDb.endTransaction(); } } if(statement != null) { statement.close(); statement = null; } // Update widget if me profile status has been modified. if(isMeProfileChanged) { WidgetUtils.kickWidgetUpdateNow(context); } return ServiceStatus.SUCCESS; } /** * Deletes all activities from the table. * * @param flag Can be a bitmap of: * <ul> * <li>{@link ActivityItem#TIMELINE_ITEM} - Timeline items</li> * <li>{@link ActivityItem#STATUS_ITEM} - Status item</li> * <li>{@link ActivityItem#ALREADY_READ} - Items that have been * read</li> * <li>NULL - to delete all activities</li> * </ul> * @param writableDb Writable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus deleteActivities(final Integer flag, final SQLiteDatabase writableDb) { DatabaseHelper.trace(true, "DatabaseHelper.deleteActivities()"); try { String whereClause = null; if (flag != null) { whereClause = Field.FLAG + "&" + flag; } if (writableDb.delete(TABLE_NAME, whereClause, null) < 0) { LogUtils.logE("ActivitiesTable.deleteActivities() " + "Unable to delete activities"); return ServiceStatus.ERROR_DATABASE_CORRUPT; } } catch (SQLException e) { LogUtils.logE("ActivitiesTable.deleteActivities() " + "Unable to delete activities", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } return ServiceStatus.SUCCESS; } public static ServiceStatus fetchNativeIdsFromLocalContactId(List<Integer > nativeItemIdList, final long localContactId, final SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper.fetchNativeIdsFromLocalContactId()"); Cursor cursor = null; try { cursor = readableDb.rawQuery("SELECT " + Field.NATIVE_ITEM_ID + " FROM " + TABLE_NAME + " WHERE " + Field.LOCAL_CONTACT_ID + " = " + localContactId, null); while (cursor.moveToNext()) { nativeItemIdList.add(cursor.getInt(0)); } } catch (SQLiteException e) { LogUtils.logE("ActivitiesTable.fetchNativeIdsFromLocalContactId()" + "Unable to fetch list of NativeIds from localcontactId", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } finally { CloseUtils.close(cursor); } return ServiceStatus.SUCCESS; } /** * Deletes specified timeline activity from the table. * * @param Context * @param timelineItem TimelineSummaryItem to be deleted * @param writableDb Writable SQLite database * @param readableDb Readable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus deleteTimelineActivity(final Context context, final TimelineSummaryItem timelineItem, final SQLiteDatabase writableDb, final SQLiteDatabase readableDb) { DatabaseHelper.trace(true, "DatabaseHelper.deleteTimelineActivity()"); try { List<Integer > nativeItemIdList = new ArrayList<Integer>() ; //Delete from Native Database if(timelineItem.mNativeThreadId != null) { //Sms Native Database final Uri smsUri = Uri.parse("content://sms"); context.getContentResolver().delete(smsUri , "thread_id=" + timelineItem.mNativeThreadId, null); //Mms Native Database final Uri mmsUri = Uri.parse("content://mms"); context.getContentResolver().delete(mmsUri, "thread_id=" + timelineItem.mNativeThreadId, null); } else { // For CallLogs if(timelineItem.mLocalContactId != null) { fetchNativeIdsFromLocalContactId(nativeItemIdList, timelineItem.mLocalContactId, readableDb); if(nativeItemIdList.size() > 0) { //CallLog Native Database for(Integer nativeItemId : nativeItemIdList) { context.getContentResolver().delete(Calls.CONTENT_URI, Calls._ID + "=" + nativeItemId, null); } } } else if ((TextUtils.isEmpty(timelineItem.mContactName)) && // we have an unknown caller (null == timelineItem.mContactId)) { context.getContentResolver().delete(Calls.CONTENT_URI, Calls._ID + "=" + timelineItem.mNativeItemId, null); } else { if(timelineItem.mContactAddress != null) { context.getContentResolver().delete(Calls.CONTENT_URI, Calls.NUMBER + "='" + timelineItem.mContactAddress+"'", null); } } } String whereClause = null; //Delete from People Client database if(timelineItem.mLocalContactId != null) { if(timelineItem.mNativeThreadId == null) { // Delete CallLogs & Chat Logs whereClause = Field.FLAG + "&" + ActivityItem.TIMELINE_ITEM + " AND " + Field.LOCAL_CONTACT_ID + "='" + timelineItem.mLocalContactId + "' AND " + Field.NATIVE_THREAD_ID + " IS NULL;"; } else { //Delete Sms/MmsLogs whereClause = Field.FLAG + "&" + ActivityItem.TIMELINE_ITEM + " AND " + Field.LOCAL_CONTACT_ID + "='" + timelineItem.mLocalContactId + "' AND " + Field.NATIVE_THREAD_ID + "=" + timelineItem.mNativeThreadId + ";"; } } else if(timelineItem.mContactAddress != null) { if(timelineItem.mNativeThreadId == null) { // Delete CallLogs & Chat Logs whereClause = Field.FLAG + "&" + ActivityItem.TIMELINE_ITEM + " AND " + Field.CONTACT_ADDRESS + "='" + timelineItem.mContactAddress + "' AND " + Field.NATIVE_THREAD_ID + " IS NULL;"; } else { //Delete Sms/MmsLogs whereClause = Field.FLAG + "&" + ActivityItem.TIMELINE_ITEM + " AND " + Field.CONTACT_ADDRESS + "='" + timelineItem.mContactAddress + "' AND " + Field.NATIVE_THREAD_ID + "=" + timelineItem.mNativeThreadId + ";"; } } else if ((TextUtils.isEmpty(timelineItem.mContactName)) && // we have an unknown caller (null == timelineItem.mContactId)) { whereClause = Field.FLAG + "&" + ActivityItem.TIMELINE_ITEM + " AND " + Field.NATIVE_ITEM_ID + "=" + timelineItem.mNativeItemId; } if (writableDb.delete(TABLE_NAME, whereClause, null) < 0) { LogUtils.logE("ActivitiesTable.deleteTimelineActivity() " + "Unable to delete specified activity"); return ServiceStatus.ERROR_DATABASE_CORRUPT; } } catch (SQLException e) { LogUtils.logE("ActivitiesTable.deleteTimelineActivity() " + "Unable to delete specified activity", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } return ServiceStatus.SUCCESS; } /** * Deletes all the timeline activities for a particular contact from the table. * * @param Context * @param timelineItem TimelineSummaryItem to be deleted * @param writableDb Writable SQLite database * @param readableDb Readable SQLite database * @return SUCCESS or a suitable error code */ public static ServiceStatus deleteTimelineActivities(Context context, TimelineSummaryItem latestTimelineItem, SQLiteDatabase writableDb, SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper.deleteTimelineActivities()"); Cursor cursor = null; try { TimelineNativeTypes[] typeList = null; //For CallLog Timeline typeList = new TimelineNativeTypes[] { TimelineNativeTypes.CallLog }; deleteTimeLineActivitiesByType(context, latestTimelineItem, writableDb, readableDb, typeList); //For SmsLog/MmsLog Timeline typeList = new TimelineNativeTypes[] { TimelineNativeTypes.SmsLog, TimelineNativeTypes.MmsLog }; deleteTimeLineActivitiesByType(context, latestTimelineItem, writableDb, readableDb, typeList); //For ChatLog Timeline typeList = new TimelineNativeTypes[] { TimelineNativeTypes.ChatLog }; deleteTimeLineActivitiesByType(context, latestTimelineItem, writableDb, readableDb, typeList); } catch (SQLException e) { LogUtils.logE("ActivitiesTable.deleteTimelineActivities() " + "Unable to delete timeline activities", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } finally { if(cursor != null) { CloseUtils.close(cursor); } } return ServiceStatus.SUCCESS; } /** * * Deletes one or more timeline items for a contact for the given types. * * @param context The app context to open the transaction in. * @param latestTimelineItem The latest item from the timeline to get the belonging contact from. * @param writableDb The database to write to. * @param readableDb The database to read from. * @param typeList The list of types to delete timeline events for. * * @return Returns a success if the transaction was successful, an error otherwise. * */ private static ServiceStatus deleteTimeLineActivitiesByType(Context context, TimelineSummaryItem latestTimelineItem, SQLiteDatabase writableDb, SQLiteDatabase readableDb, TimelineNativeTypes[] typeList) { Cursor cursor = null; TimelineSummaryItem timelineItem = null; try { if ((!TextUtils.isEmpty(latestTimelineItem.mContactName)) && (latestTimelineItem.mContactId != null)) { cursor = fetchTimelineEventsForContact(0L, latestTimelineItem.mLocalContactId, latestTimelineItem.mContactName, typeList, null, readableDb); if(cursor != null && cursor.getCount() > 0) { cursor.moveToFirst(); timelineItem = getTimelineData(cursor); if(timelineItem != null) { return deleteTimelineActivity(context, timelineItem, writableDb, readableDb); } } } else { // contact id and name are null or empty, so it is an unknown contact return deleteTimelineActivity(context, latestTimelineItem, writableDb, readableDb); } } catch (SQLException e) { LogUtils.logE("ActivitiesTable.deleteTimelineActivities() " + "Unable to delete timeline activities", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } finally { if(cursor != null) { CloseUtils.close(cursor); } } return ServiceStatus.SUCCESS; } /** * Fetches timeline events grouped by local contact ID, name or contact * address. Events returned will be in reverse-chronological order. If a * native type list is provided the result will be filtered by the list. * * @param minTimeStamp Only timeline events from this date will be returned * @param nativeTypes A list of native types to filter the result, or null * to return all. * @param readableDb Readable SQLite database * @return A cursor containing the result. The * {@link #getTimelineData(Cursor)} method should be used for * reading the cursor. */ public static Cursor fetchTimelineEventList(final Long minTimeStamp, final TimelineNativeTypes[] nativeTypes, final SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper.fetchTimelineEventList() " + "minTimeStamp[" + minTimeStamp + "]"); try { int andVal = LATEST_STATUS_FOR_ALL; String typesQuery = " AND "; if (nativeTypes != null) { typesQuery += DatabaseHelper.createWhereClauseFromList( Field.NATIVE_ITEM_TYPE.toString(), nativeTypes, "OR"); typesQuery += " AND "; if (nativeTypes[0] != TimelineNativeTypes.CallLog){ andVal = LATEST_STATUS_FOR_TYPE; } else { // Francisco: FIXME: This is actually the fix for PAND-2462 // But this code and this latest contact status are a mystery andVal |= LATEST_STATUS_FOR_TYPE; } } String query = "SELECT " + Field.LOCAL_ACTIVITY_ID + "," + Field.TIMESTAMP + "," + Field.CONTACT_NAME + "," + Field.CONTACT_AVATAR_URL + "," + Field.LOCAL_CONTACT_ID + "," + Field.TITLE + "," + Field.DESCRIPTION + "," + Field.CONTACT_NETWORK + "," + Field.NATIVE_ITEM_TYPE + "," + Field.NATIVE_ITEM_ID + "," + Field.TYPE + "," + Field.CONTACT_ID + "," + Field.USER_ID + "," + Field.NATIVE_THREAD_ID + "," + Field.CONTACT_ADDRESS + "," + Field.INCOMING + " FROM " + TABLE_NAME + " WHERE (" + Field.FLAG + "&" + ActivityItem.TIMELINE_ITEM + ")" + typesQuery + Field.TIMESTAMP + " > " + minTimeStamp + " AND (" + Field.LATEST_CONTACT_STATUS + " & " + andVal + ") ORDER BY " + Field.TIMESTAMP + " DESC"; return readableDb.rawQuery(query, null); } catch (SQLiteException e) { LogUtils.logE("ActivitiesTable.fetchLastUpdateTime() " + "Unable to fetch timeline event list", e); return null; } } /** * Adds a list of timeline events to the database. Each event is grouped by * contact and grouped by contact + native type. * * @param itemList List of timeline events * @param isCallLog true to group all activities with call logs, false to * group with messaging * @param writableDb Writable SQLite database * @return SUCCESS or a suitable error */ public static ServiceStatus addTimelineEvents( final ArrayList<TimelineSummaryItem> itemList, final boolean isCallLog, final SQLiteDatabase writableDb) { DatabaseHelper.trace(true, "DatabaseHelper.addTimelineEvents()"); TimelineNativeTypes[] activityTypes; if (isCallLog) { activityTypes = new TimelineNativeTypes[] { TimelineNativeTypes.CallLog }; } else { activityTypes = new TimelineNativeTypes[] { TimelineNativeTypes.SmsLog, TimelineNativeTypes.MmsLog }; } for (TimelineSummaryItem item : itemList) { try { writableDb.beginTransaction(); if (findNativeActivity(item, writableDb)) { continue; } int latestStatusVal = 0; if (!TextUtils.isEmpty(item.mContactName) || item.mLocalContactId != null) { latestStatusVal |= removeContactGroup(item.mLocalContactId, item.mContactName, item.mTimestamp, ActivityItem.TIMELINE_ITEM, null, writableDb); latestStatusVal |= removeContactGroup(item.mLocalContactId, item.mContactName, item.mTimestamp, ActivityItem.TIMELINE_ITEM, activityTypes, writableDb); } else { // unknown contact latestStatusVal = LATEST_STATUS_FOR_ALL; } ContentValues values = new ContentValues(); values.put(Field.CONTACT_NAME.toString(), item.mContactName); values.put(Field.CONTACT_ID.toString(), item.mContactId); values.put(Field.USER_ID.toString(), item.mUserId); values.put(Field.LOCAL_CONTACT_ID.toString(), item.mLocalContactId); values.put(Field.CONTACT_NETWORK.toString(), item.mContactNetwork); values.put(Field.DESCRIPTION.toString(), item.mDescription); values.put(Field.TITLE.toString(), item.mTitle); values.put(Field.CONTACT_ADDRESS.toString(), item.mContactAddress); values.put(Field.FLAG.toString(), ActivityItem.TIMELINE_ITEM); values.put(Field.NATIVE_ITEM_ID.toString(), item.mNativeItemId); values.put(Field.NATIVE_ITEM_TYPE.toString(), item.mNativeItemType); values.put(Field.TIMESTAMP.toString(), item.mTimestamp); if (item.mType != null) { values.put(Field.TYPE.toString(), item.mType.getTypeCode()); } values.put(Field.LATEST_CONTACT_STATUS.toString(), latestStatusVal); values.put(Field.NATIVE_THREAD_ID.toString(), item.mNativeThreadId); if (item.mIncoming != null) { values.put(Field.INCOMING.toString(), item.mIncoming.ordinal()); } item.mLocalActivityId = writableDb.insert(TABLE_NAME, null, values); if (item.mLocalActivityId < 0) { LogUtils.logE("ActivitiesTable.addTimelineEvents() " + "ERROR_DATABASE_CORRUPT - Unable to add " + "timeline list to database"); return ServiceStatus.ERROR_DATABASE_CORRUPT; } writableDb.setTransactionSuccessful(); } catch (SQLException e) { LogUtils.logE("ActivitiesTable.addTimelineEvents() SQLException - " + "Unable to add timeline list to database", e); return ServiceStatus.ERROR_DATABASE_CORRUPT; } finally { writableDb.endTransaction(); } } return ServiceStatus.SUCCESS; } /** * The method returns the ROW_ID i.e. the INTEGER PRIMARY KEY AUTOINCREMENT * field value for the inserted row, i.e. LOCAL_ID. * * @param item TimelineSummaryItem. * @param read - TRUE if the chat message is outgoing or gets into the * timeline history view for a contact with LocalContactId. * @param writableDb Writable SQLite database. * @return LocalContactID or -1 if the row was not inserted. */ public static long addChatTimelineEvent(final TimelineSummaryItem item, final boolean read, final SQLiteDatabase writableDb) { DatabaseHelper.trace(true, "DatabaseHelper.addChatTimelineEvent()"); try { writableDb.beginTransaction(); int latestStatusVal = 0; if (item.mContactName != null || item.mLocalContactId != null) { latestStatusVal |= removeContactGroup(item.mLocalContactId, item.mContactName, item.mTimestamp, ActivityItem.TIMELINE_ITEM, null, writableDb); latestStatusVal |= removeContactGroup(item.mLocalContactId, item.mContactName, item.mTimestamp, ActivityItem.TIMELINE_ITEM, new TimelineNativeTypes[] { TimelineNativeTypes.ChatLog }, writableDb); } ContentValues values = new ContentValues(); values.put(Field.CONTACT_NAME.toString(), item.mContactName); values.put(Field.CONTACT_ID.toString(), item.mContactId); values.put(Field.USER_ID.toString(), item.mUserId); values.put(Field.LOCAL_CONTACT_ID.toString(), item.mLocalContactId); values.put(Field.CONTACT_NETWORK.toString(), item.mContactNetwork); values.put(Field.DESCRIPTION.toString(), item.mDescription); /** Chat message body. **/ values.put(Field.TITLE.toString(), item.mTitle); values.put(Field.CONTACT_ADDRESS.toString(), item.mContactAddress); if (read) { values.put(Field.FLAG.toString(), ActivityItem.TIMELINE_ITEM | ActivityItem.ALREADY_READ); } else { values.put(Field.FLAG.toString(), ActivityItem.TIMELINE_ITEM | 0); } values.put(Field.NATIVE_ITEM_ID.toString(), item.mNativeItemId); values.put(Field.NATIVE_ITEM_TYPE.toString(), item.mNativeItemType); values.put(Field.TIMESTAMP.toString(), item.mTimestamp); if (item.mType != null) { values.put(Field.TYPE.toString(), item.mType.getTypeCode()); } values.put(Field.LATEST_CONTACT_STATUS.toString(), latestStatusVal); values.put(Field.NATIVE_THREAD_ID.toString(), item.mNativeThreadId); /** Conversation ID for chat message. **/ // values.put(Field.URI.toString(), item.conversationId); // 0 for incoming, 1 for outgoing if (item.mIncoming != null) { values.put(Field.INCOMING.toString(), item.mIncoming.ordinal()); } final long itemId = writableDb.insert(TABLE_NAME, null, values); if (itemId < 0) { LogUtils.logE("ActivitiesTable.addTimelineEvents() - " + "Unable to add timeline list to database, index<0:" + itemId); return -1; } writableDb.setTransactionSuccessful(); return itemId; } catch (SQLException e) { LogUtils.logE("ActivitiesTable.addTimelineEvents() SQLException - " + "Unable to add timeline list to database", e); return -1; } finally { writableDb.endTransaction(); } } /** * Clears the grouping of an activity in the database, if the given activity * is newer. This is so that only the latest activity is returned for a * particular group. Each activity is associated with two groups: * <ol> * <li>All group - Grouped by contact only</li> * <li>Native group - Grouped by contact and native type (call log or * messaging)</li> * </ol> * The group to be removed is determined by the activityTypes parameter. * Grouping must also work for timeline events that are not associated with * a contact. The following fields are used to do identify a contact for the * grouping (in order of priority): * <ol> * <li>localContactId - If it not null</li> * <li>name - If this is a valid telephone number, the match will be done to * ensure that the same phone number written in different ways will be * included in the group. See {@link #fetchNameWhereClause(Long, String)} * for more information.</li> * </ol> * * @param localContactId Local contact Id or NULL if the activity is not * associated with a contact. * @param name Name of contact or contact address (telephone number, email, * etc). * @param newUpdateTime The time that the given activity has occurred * @param flag Bitmap of types including: * <ul> * <li>{@link ActivityItem#TIMELINE_ITEM} - Timeline items</li> * <li>{@link ActivityItem#STATUS_ITEM} - Status item</li> * <li>{@link ActivityItem#ALREADY_READ} - Items that have been * read</li> * </ul> * @param activityTypes A list of native types to include in the grouping. * Currently, only two groups are supported (see above). If this * parameter is null the contact will be added to the * "all group", otherwise the contact is added to the native * group. * @param writableDb Writable SQLite database * @return The latest contact status value which should be added to the * current activities grouping. */ private static int removeContactGroup(final Long localContactId, final String name, final Long newUpdateTime, final int flag, final TimelineNativeTypes[] activityTypes, final SQLiteDatabase writableDb) { String whereClause = ""; int andVal = LATEST_STATUS_FOR_ALL; if (activityTypes != null) { whereClause = DatabaseHelper.createWhereClauseFromList( Field.NATIVE_ITEM_TYPE.toString(), activityTypes, "OR"); whereClause += " AND "; andVal = LATEST_STATUS_FOR_TYPE; } String nameWhereClause = fetchNameWhereClause(localContactId, name); if (nameWhereClause == null) { return 0; } whereClause += nameWhereClause; Long prevTime = null; Long prevLocalId = null; Integer prevLatestContactStatus = null; Cursor cursor = null; try { cursor = writableDb.rawQuery("SELECT " + Field.TIMESTAMP + "," + Field.LOCAL_ACTIVITY_ID + "," + Field.LATEST_CONTACT_STATUS + " FROM " + TABLE_NAME + " WHERE " + whereClause + " AND " + "(" + Field.LATEST_CONTACT_STATUS + " & " + andVal + ") AND (" + Field.FLAG + "&" + flag + ") ORDER BY " + Field.TIMESTAMP + " DESC", null); if (cursor.moveToFirst()) { prevTime = cursor.getLong(0); prevLocalId = cursor.getLong(1); prevLatestContactStatus = cursor.getInt(2); } } catch (SQLException e) { return 0; } finally { CloseUtils.close(cursor); } if (prevTime != null && newUpdateTime != null) { if (newUpdateTime >= prevTime) { ContentValues cv = new ContentValues(); cv.put(Field.LATEST_CONTACT_STATUS.toString(), prevLatestContactStatus & (~andVal)); if (writableDb.update(TABLE_NAME, cv, Field.LOCAL_ACTIVITY_ID + "=" + prevLocalId, null) <= 0) { LogUtils.logE("ActivitiesTable.addTimelineEvents() " + "Unable to update timeline as the latest"); return 0; } return andVal; } else { return 0; } } return andVal; } /** * Checks if an activity exists in the database. * * @param item The native SMS item to check against our client activities DB table. * * @return true if the activity was found, false otherwise * */ private static boolean findNativeActivity(TimelineSummaryItem item, final SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper.findNativeActivity()"); Cursor cursor = null; boolean result = false; try { final String[] args = { Integer.toString(item.mNativeItemId), Integer.toString(item.mNativeItemType), Long.toString(item.mTimestamp) }; cursor = readableDb.rawQuery("SELECT " + Field.ACTIVITY_ID + " FROM " + TABLE_NAME + " WHERE " + Field.NATIVE_ITEM_ID + "=? AND " + Field.NATIVE_ITEM_TYPE + "=?" + " AND " + Field.TIMESTAMP + "=?", args); if (cursor.moveToFirst()) { result = true; } } finally { CloseUtils.close(cursor); } return result; } /** * Returns a string which can be added to the where clause in an SQL query * on the activities table, to filter the result for a specific contact or * name. The clause will prioritise in the following way: * <ol> * <li>Use localContactId - If it not null</li> * <li>Use name - If this is a valid telephone number, the match will be * done to ensure that the same phone number written in different ways will * be included in the group. * </ol> * * @param localContactId The local contact ID, or null if the contact does * not exist in the People database. * @param name A string containing the name, or a telephone number/email * identifying the contact. * @return The where clause string */ private static String fetchNameWhereClause(final Long localContactId, final String name) { DatabaseHelper.trace(false, "DatabaseHelper.fetchNameWhereClause()"); if (localContactId != null) { return Field.LOCAL_CONTACT_ID + "=" + localContactId; } if (name == null) { return "1=1"; // we need to return something that evaluates to true as this method is called after a SQL AND-operator } final String searchName = DatabaseUtils.sqlEscapeString(name); if (PhoneNumberUtils.isWellFormedSmsAddress(name)) { return "(PHONE_NUMBERS_EQUAL(" + Field.CONTACT_NAME + "," + searchName + ") OR "+ Field.CONTACT_NAME + "=" + searchName+")"; } else { return Field.CONTACT_NAME + "=" + searchName; } } /** * Returns the timeline summary data from the current location of the given * cursor. The cursor can be obtained using * {@link #fetchTimelineEventList(Long, TimelineNativeTypes[], * SQLiteDatabase)} or * {@link #fetchTimelineEventsForContact(Long, Long, String, * TimelineNativeTypes[], SQLiteDatabase)}. * * @param cursor Cursor in the required position. * @return A filled out TimelineSummaryItem object */ public static TimelineSummaryItem getTimelineData(final Cursor cursor) { DatabaseHelper.trace(false, "DatabaseHelper.getTimelineData()"); TimelineSummaryItem item = new TimelineSummaryItem(); item.mLocalActivityId = SqlUtils.setLong(cursor, Field.LOCAL_ACTIVITY_ID.toString(), null); item.mTimestamp = SqlUtils.setLong(cursor, Field.TIMESTAMP.toString(), null); item.mContactName = SqlUtils.setString(cursor, Field.CONTACT_NAME.toString()); if (!cursor.isNull( cursor.getColumnIndex(Field.CONTACT_AVATAR_URL.toString()))) { item.mHasAvatar = true; } item.mLocalContactId = SqlUtils.setLong(cursor, Field.LOCAL_CONTACT_ID.toString(), null); item.mTitle = SqlUtils.setString(cursor, Field.TITLE.toString()); item.mDescription = SqlUtils.setString(cursor, Field.DESCRIPTION.toString()); item.mContactNetwork = SqlUtils.setString(cursor, Field.CONTACT_NETWORK.toString()); item.mNativeItemType = SqlUtils.setInt(cursor, Field.NATIVE_ITEM_TYPE.toString(), null); item.mNativeItemId = SqlUtils.setInt(cursor, Field.NATIVE_ITEM_ID.toString(), null); item.mType = SqlUtils.setActivityItemType(cursor, Field.TYPE.toString()); item.mContactId = SqlUtils.setLong(cursor, Field.CONTACT_ID.toString(), null); item.mUserId = SqlUtils.setLong(cursor, Field.USER_ID.toString(), null); item.mNativeThreadId = SqlUtils.setInt(cursor, Field.NATIVE_THREAD_ID.toString(), null); item.mContactAddress = SqlUtils.setString(cursor, Field.CONTACT_ADDRESS.toString()); item.mIncoming = SqlUtils.setTimelineSummaryItemType(cursor, Field.INCOMING.toString()); return item; } /** * Fetches timeline events for a specific contact identified by local * contact ID, name or address. Events returned will be in * reverse-chronological order. If a native type list is provided the result * will be filtered by the list. * * @param timeStamp Only events from this time will be returned * @param localContactId The local contact ID if the contact is in the * People database, or null. * @param name The name or address of the contact (required if local contact * ID is NULL). * @param nativeTypes A list of required native types to filter the result, * or null to return all timeline events for the contact. * @param readableDb Readable SQLite database * @param networkName The name of the network the contacts belongs to * (required in order to provide appropriate chat messages * filtering) If the parameter is null messages from all networks * will be returned. The values are the * SocialNetwork.VODAFONE.toString(), * SocialNetwork.GOOGLE.toString(), or * SocialNetwork.MICROSOFT.toString() results. * @return The cursor that can be read using * {@link #getTimelineData(Cursor)}. */ public static Cursor fetchTimelineEventsForContact(final Long timeStamp, final Long localContactId, final String name, final TimelineNativeTypes[] nativeTypes, final String networkName, final SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "fetchTimelineEventsForContact()"); try { String typesQuery = " AND "; if (nativeTypes.length > 0) { StringBuffer typesQueryBuffer = new StringBuffer(); typesQueryBuffer.append(" AND ("); for (int i = 0; i < nativeTypes.length; i++) { final TimelineNativeTypes type = nativeTypes[i]; typesQueryBuffer.append(Field.NATIVE_ITEM_TYPE + "=" + type.ordinal()); if (i < nativeTypes.length - 1) { typesQueryBuffer.append(" OR "); } } typesQueryBuffer.append(") AND "); typesQuery = typesQueryBuffer.toString(); } /** Filter by account. **/ String networkQuery = ""; String queryNetworkName = networkName; if (queryNetworkName != null) { networkQuery = Field.CONTACT_NETWORK + "='" + queryNetworkName + "' AND "; } String whereAppend; if (localContactId == null) { whereAppend = Field.LOCAL_CONTACT_ID + " IS NULL AND " + fetchNameWhereClause(localContactId, name); } else { whereAppend = Field.LOCAL_CONTACT_ID + "=" + localContactId; } String query = "SELECT " + Field.LOCAL_ACTIVITY_ID + "," + Field.TIMESTAMP + "," + Field.CONTACT_NAME + "," + Field.CONTACT_AVATAR_URL + "," + Field.LOCAL_CONTACT_ID + "," + Field.TITLE + "," + Field.DESCRIPTION + "," + Field.CONTACT_NETWORK + "," + Field.NATIVE_ITEM_TYPE + "," + Field.NATIVE_ITEM_ID + "," + Field.TYPE + "," + Field.CONTACT_ID + "," + Field.USER_ID + "," + Field.NATIVE_THREAD_ID + "," + Field.CONTACT_ADDRESS + "," + Field.INCOMING + " FROM " + TABLE_NAME + " WHERE (" + Field.FLAG + "&" + ActivityItem.TIMELINE_ITEM + ")" + typesQuery + networkQuery + whereAppend + " ORDER BY " + Field.TIMESTAMP + " ASC"; return readableDb.rawQuery(query, null); } catch (SQLiteException e) { LogUtils.logE("ActivitiesTable.fetchTimelineEventsForContact() " + "Unable to fetch timeline event for contact list", e); return null; } } /** * Mark the chat timeline events for a given contact as read. * * @param localContactId Local contact ID. * @param networkName Name of the SNS. * @param writableDb Writable SQLite reference. * @return Number of rows affected by database update. */ public static int markChatTimelineEventsForContactAsRead( final Long localContactId, final String networkName, final SQLiteDatabase writableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "fetchTimelineEventsForContact()"); ContentValues values = new ContentValues(); values.put(Field.FLAG.toString(), ActivityItem.TIMELINE_ITEM | ActivityItem.ALREADY_READ); String networkQuery = ""; if (networkName != null) { networkQuery = " AND (" + Field.CONTACT_NETWORK + "='" + networkName + "')"; } final String where = Field.LOCAL_CONTACT_ID + "=" + localContactId + " AND " + Field.NATIVE_ITEM_TYPE + "=" + TimelineNativeTypes.ChatLog.ordinal() + " AND (" /** * If the network is null, set all messages as read for this * contact. */ + Field.FLAG + "=" + ActivityItem.TIMELINE_ITEM + ")" + networkQuery; return writableDb.update(TABLE_NAME, values, where, null); } /** * Returns the latest status event for a contact with the given local contact id. * * @param local contact id Server contact ID. * @param readableDb Readable SQLite database. * @return The ActivityItem representing the latest status event for given local contact id, * or null if nothing was found. */ public static ActivityItem getLatestStatusForContact(final long localContactId, final SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper getLatestStatusForContact"); Cursor cursor = null; try { StringBuffer query = StringBufferPool.getStringBuffer(SQLKeys.SELECT); query.append(getFullQueryList()).append(SQLKeys.FROM).append(TABLE_NAME).append(SQLKeys.WHERE). append(Field.FLAG).append('&').append(ActivityItem.STATUS_ITEM).append(SQLKeys.AND). append(Field.LOCAL_CONTACT_ID).append('=').append(localContactId). append(" ORDER BY ").append(Field.TIMESTAMP).append(" DESC"); cursor = readableDb.rawQuery(StringBufferPool.toStringThenRelease(query), null); if (cursor.moveToFirst()) { final ActivityItem activityItem = new ActivityItem(); final ActivityContact activityContact = new ActivityContact(); ActivitiesTable.getQueryData(cursor, activityItem, activityContact); return activityItem; } } finally { CloseUtils.close(cursor); cursor = null; } return null; } /** * Updates timeline when a new contact is added to the People database. * Updates all timeline events that are not associated with a contact and * have a phone number that matches the oldName parameter. * * @param oldName The telephone number (since is the name of an activity * that is not associated with a contact) * @param newName The new name * @param newLocalContactId The local Contact Id for the added contact. * @param newContactId The server Contact Id for the added contact (or null * if the contact has not yet been synced). * @param witeableDb Writable SQLite database */ public static void updateTimelineContactNameAndId(final String oldName, final String newName, final Long newLocalContactId, final Long newContactId, final SQLiteDatabase witeableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "updateTimelineContactNameAndId()"); try { ContentValues values = new ContentValues(); if (newName != null) { values.put(Field.CONTACT_NAME.toString(), newName); } else { LogUtils.logE("updateTimelineContactNameAndId() " + "newName should never be null"); } if (newLocalContactId != null) { values.put(Field.LOCAL_CONTACT_ID.toString(), newLocalContactId); } else { LogUtils.logE("updateTimelineContactNameAndId() " + "newLocalContactId should never be null"); } if (newContactId != null) { values.put(Field.CONTACT_ID.toString(), newContactId); } else { /** * newContactId will be null if adding a contact from the UI. */ LogUtils.logI("updateTimelineContactNameAndId() " + "newContactId is null"); /** * We haven't got server Contact it, it means it haven't been * synced yet. */ } String name = ""; if (oldName != null) { name = oldName; LogUtils.logW("ActivitiesTable." + "updateTimelineContactNameAndId() oldName is NULL"); } String[] args = { "2", name }; String whereClause = Field.LOCAL_CONTACT_ID + " IS NULL AND " + Field.FLAG + "=? AND PHONE_NUMBERS_EQUAL(" + Field.CONTACT_ADDRESS + ",?)"; witeableDb.update(TABLE_NAME, values, whereClause, args); } catch (SQLException e) { LogUtils.logE("ActivitiesTable.updateTimelineContactNameAndId() " + "Unable update table", e); throw e; } } /** * Updates the timeline when a contact name is modified in the database. * * @param newName The new name. * @param localContactId Local contact Id which was modified. * @param witeableDb Writable SQLite database */ public static void updateTimelineContactNameAndId(final String newName, final Long localContactId, final SQLiteDatabase witeableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "updateTimelineContactNameAndId()"); if (newName == null || localContactId == null) { LogUtils.logE("updateTimelineContactNameAndId() newName or " + "localContactId == null newName(" + newName + ") localContactId(" + localContactId + ")"); return; } try { ContentValues values = new ContentValues(); Long cId = ContactsTable.fetchServerId(localContactId, witeableDb); values.put(Field.CONTACT_NAME.toString(), newName); if (cId != null) { values.put(Field.CONTACT_ID.toString(), cId); } String[] args = { localContactId.toString() }; String whereClause = Field.LOCAL_CONTACT_ID + "=?"; witeableDb.update(TABLE_NAME, values, whereClause, args); } catch (SQLException e) { LogUtils.logE("ActivitiesTable.updateTimelineContactNameAndId()" + " Unable update table", e); } } /** * Updates the timeline entries in the activities table to remove deleted * contact info. * * @param localContactId - the contact id that has been deleted * @param writeableDb - reference to the database */ public static void removeTimelineContactData(final Long localContactId, final SQLiteDatabase writeableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "removeTimelineContactData()"); if (localContactId == null) { LogUtils.logE("removeTimelineContactData() localContactId == null " + "localContactId(" + localContactId + ")"); return; } try { //Remove all the Chat Entries removeChatTimelineForContact(localContactId, writeableDb); String[] args = { localContactId.toString() }; String query = "UPDATE " + TABLE_NAME + " SET " + Field.LOCAL_CONTACT_ID + "=NULL, " + Field.CONTACT_ID + "=NULL, " + Field.CONTACT_NAME + "=" + Field.CONTACT_ADDRESS + " WHERE " + Field.LOCAL_CONTACT_ID + "=? AND (" + Field.FLAG + "&" + ActivityItem.TIMELINE_ITEM + ")"; writeableDb.execSQL(query, args); } catch (SQLException e) { LogUtils.logE("ActivitiesTable.removeTimelineContactData() Unable " + "to update table: \n", e); } } /** * Removes all the items from the chat timeline for the given contact. * * @param localContactId Given contact ID. * @param writeableDb Writable SQLite database. */ private static void removeChatTimelineForContact( final Long localContactId, final SQLiteDatabase writeableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "removeChatTimelineForContact()"); if (localContactId == null || (localContactId == -1)) { LogUtils.logE("removeChatTimelineForContact() localContactId == " + "null " + "localContactId(" + localContactId + ")"); return; } final String query = Field.LOCAL_CONTACT_ID + "=" + localContactId + " AND (" + Field.NATIVE_ITEM_TYPE + "=" + TimelineNativeTypes.ChatLog.ordinal() + ")"; try { writeableDb.delete(TABLE_NAME, query, null); } catch (SQLException e) { LogUtils.logE("ActivitiesTable.removeChatTimelineForContact() " + "Unable to update table", e); } } /** * Removes items from the chat timeline that are not for the given contact. * * @param localContactId Given contact ID. * @param writeableDb Writable SQLite database. */ public static void removeChatTimelineExceptForContact( final Long localContactId, final SQLiteDatabase writeableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "removeTimelineContactData()"); if (localContactId == null || (localContactId == -1)) { LogUtils.logE("removeTimelineContactData() localContactId == null " + "localContactId(" + localContactId + ")"); return; } try { final long olderThan = System.currentTimeMillis() - Settings.HISTORY_IS_WEEK_LONG; final String query = Field.LOCAL_CONTACT_ID + "!=" + localContactId + " AND (" + Field.NATIVE_ITEM_TYPE + "=" + TimelineNativeTypes.ChatLog.ordinal() + ") AND (" + Field.TIMESTAMP + "<" + olderThan + ")"; writeableDb.delete(TABLE_NAME, query, null); } catch (SQLException e) { LogUtils.logE("ActivitiesTable.removeTimelineContactData() " + "Unable to update table", e); } } /*** * Returns the number of users have currently have unread chat messages. * * @param readableDb Reference to a readable database. * @return Number of users with unread chat messages. */ public static int getNumberOfUnreadChatUsers( final SQLiteDatabase readableDb) { final String query = "SELECT " + Field.LOCAL_CONTACT_ID + " FROM " + TABLE_NAME + " WHERE " + Field.NATIVE_ITEM_TYPE + "=" + TimelineNativeTypes.ChatLog.ordinal() + " AND (" /** * This condition below means the timeline is not yet marked * as READ. */ + Field.FLAG + "=" + ActivityItem.TIMELINE_ITEM + ")"; Cursor cursor = null; try { cursor = readableDb.rawQuery(query, null); ArrayList<Long> ids = new ArrayList<Long>(); Long id = null; while (cursor.moveToNext()) { id = cursor.getLong(0); if (!ids.contains(id)) { ids.add(id); } } return ids.size(); } finally { CloseUtils.close(cursor); } } /*** * Returns the number of unread chat messages. * * @param readableDb Reference to a readable database. * @return Number of unread chat messages. */ public static int getNumberOfUnreadChatMessages( final SQLiteDatabase readableDb) { final String query = "SELECT " + Field.ACTIVITY_ID + " FROM " + TABLE_NAME + " WHERE " + Field.NATIVE_ITEM_TYPE + "=" + TimelineNativeTypes.ChatLog.ordinal() + " AND (" + Field.FLAG + "=" + ActivityItem.TIMELINE_ITEM + ")"; Cursor cursor = null; try { cursor = readableDb.rawQuery(query, null); return cursor.getCount(); } finally { CloseUtils.close(cursor); } } /*** * Returns the number of unread chat messages for this contact besides this * network. * * @param localContactId Given contact ID. * @param network SNS name. * @param readableDb Reference to a readable database. * @return Number of unread chat messages. */ public static int getNumberOfUnreadChatMessagesForContactAndNetwork( final long localContactId, final String network, final SQLiteDatabase readableDb) { final String query = "SELECT " + Field.ACTIVITY_ID + " FROM " + TABLE_NAME + " WHERE " + Field.NATIVE_ITEM_TYPE + "=" + TimelineNativeTypes.ChatLog.ordinal() + " AND (" + Field.FLAG + "=" + ActivityItem.TIMELINE_ITEM + ") AND (" + Field.LOCAL_CONTACT_ID + "=" + localContactId + ") AND (" + Field.CONTACT_NETWORK + "!=\"" + network + "\")"; Cursor cursor = null; try { cursor = readableDb.rawQuery(query, null); return cursor.getCount(); } finally { CloseUtils.close(cursor); } } /*** * Sets all chat messages to already read * * @param writableDb Reference to a writable database. * @return void. */ public static void setAllChatMessagesToRead( final SQLiteDatabase writableDb) { ContentValues values = new ContentValues(); values.put(Field.FLAG.toString(), ActivityItem.TIMELINE_ITEM | ActivityItem.ALREADY_READ); final String where = Field.NATIVE_ITEM_TYPE + "=" + TimelineNativeTypes.ChatLog.ordinal() + " AND (" + Field.FLAG + "=" + ActivityItem.TIMELINE_ITEM + ")"; writableDb.update(TABLE_NAME, values, where, null); } /*** * Returns the newest unread chat message. * * @param readableDb Reference to a readable database. * @return TimelineSummaryItem of the newest unread chat message, or NULL if * none are found. */ public static TimelineSummaryItem getNewestUnreadChatMessage( final SQLiteDatabase readableDb) { final String query = "SELECT " + Field.LOCAL_ACTIVITY_ID + "," + Field.TIMESTAMP + "," + Field.CONTACT_NAME + "," + Field.CONTACT_AVATAR_URL + "," + Field.LOCAL_CONTACT_ID + "," + Field.TITLE + "," + Field.DESCRIPTION + "," + Field.CONTACT_NETWORK + "," + Field.NATIVE_ITEM_TYPE + "," + Field.NATIVE_ITEM_ID + "," + Field.TYPE + "," + Field.CONTACT_ID + "," + Field.USER_ID + "," + Field.NATIVE_THREAD_ID + "," + Field.CONTACT_ADDRESS + "," + Field.INCOMING + " FROM " + TABLE_NAME + " WHERE " + Field.NATIVE_ITEM_TYPE + "=" + TimelineNativeTypes.ChatLog.ordinal() + " AND (" + Field.FLAG + "=" + ActivityItem.TIMELINE_ITEM + ")"; Cursor cursor = null; try { cursor = readableDb.rawQuery(query, null); long max = 0; long time = 0; int index = -1; while (cursor.moveToNext()) { time = SqlUtils.setLong(cursor, Field.TIMESTAMP.toString(), -1L); if (time > max) { max = time; index = cursor.getPosition(); } } if (index != -1) { cursor.moveToPosition(index); return getTimelineData(cursor); } else { return null; } } finally { CloseUtils.close(cursor); } } /*** * Cleanup the Activity Table by deleting anything older than * CLEANUP_MAX_AGE_DAYS, or preventing the total size from exceeding * CLEANUP_MAX_QUANTITY. * * @param writableDb Reference to a writable SQLite Database. */ public static void cleanupActivityTable(final SQLiteDatabase writableDb) { DatabaseHelper.trace(true, "DatabaseHelper.cleanupActivityTable()"); try { /* * Delete any Activities older than CLEANUP_MAX_AGE_DAYS days. */ if (CLEANUP_MAX_AGE_DAYS != -1) { if (writableDb.delete(TABLE_NAME, Field.TIMESTAMP + " < " + ((System.currentTimeMillis() / NUMBER_OF_MS_IN_A_SECOND) - CLEANUP_MAX_AGE_DAYS * NUMBER_OF_MS_IN_A_DAY), null) < 0) { LogUtils.logE("ActivitiesTable.cleanupActivityTable() " + "Unable to cleanup Activities table by date"); } } /* * Delete oldest Activities, when total number of rows exceeds * CLEANUP_MAX_QUANTITY in quantity. */ if (CLEANUP_MAX_QUANTITY != -1) { writableDb.execSQL("DELETE FROM " + TABLE_NAME + " WHERE " + Field.LOCAL_ACTIVITY_ID + " IN (SELECT " + Field.LOCAL_ACTIVITY_ID + " FROM " + TABLE_NAME + " ORDER BY " + Field.TIMESTAMP + " DESC LIMIT -1 OFFSET " + CLEANUP_MAX_QUANTITY + ")"); } } catch (SQLException e) { LogUtils.logE("ActivitiesTable.cleanupActivityTable() " + "Unable to cleanup Activities table by date", e); } } /** * Returns the TimelineSummaryItem for the corresponding native thread Id. * * @param threadId native thread id * @param readableDb Readable SQLite database * @return The TimelineSummaryItem of the matching native thread id, * or NULL if none are found. */ public static TimelineSummaryItem fetchTimeLineDataFromNativeThreadId( final String threadId, final SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "fetchTimeLineDataFromNativeThreadId()"); Cursor cursor = null; try { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + Field.NATIVE_THREAD_ID + " = " + threadId + " ORDER BY " + Field.TIMESTAMP + " DESC"; cursor = readableDb.rawQuery(query, null); if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0)) { return getTimelineData(cursor); } else { return null; } } finally { CloseUtils.close(cursor); } } /** * This method deletes the timeline event for the contact by timestamp. * * @param localContactId Given contact ID. * @param the time of the event. * @param writeableDb Writable SQLite database. */ public static void deleteUnsentChatMessageForContact( final Long localContactId, long timestamp, final SQLiteDatabase writeableDb) { DatabaseHelper.trace(false, "ActivitiesTable deleteUnsentChatMessageForContact()"); if (localContactId == null || (localContactId == -1)) { LogUtils.logE("deleteUnsentChatMessageForContact() localContactId == " + "null " + "localContactId(" + localContactId + ")"); return; } StringBuffer where1 = StringBufferPool.getStringBuffer(Field.LOCAL_CONTACT_ID.toString()); where1.append("=").append(localContactId).append(" AND (").append(Field.NATIVE_ITEM_TYPE.toString()).append("=") .append(TimelineNativeTypes.ChatLog.ordinal()).append(") AND (").append(Field.TIMESTAMP).append("=") .append(timestamp).append(") AND (").append(Field.INCOMING).append("=") .append(TimelineSummaryItem.Type.OUTGOING.ordinal()).append(")"); if (writeableDb.delete(TABLE_NAME, StringBufferPool.toStringThenRelease(where1), null) > 0) { StringBuffer where2 = StringBufferPool.getStringBuffer(Field.LOCAL_ACTIVITY_ID.toString()); where2.append(" IN (SELECT ").append(Field.LOCAL_ACTIVITY_ID.toString()).append(" FROM ").append(TABLE_NAME) .append(" WHERE ").append(Field.LOCAL_CONTACT_ID.toString()).append("=").append(localContactId).append(" AND ") .append(Field.NATIVE_ITEM_TYPE).append(" IN (").append(TimelineNativeTypes.CallLog.ordinal()).append(",") .append(TimelineNativeTypes.SmsLog.ordinal()).append(",").append(TimelineNativeTypes.MmsLog.ordinal()) .append(",").append(TimelineNativeTypes.ChatLog.ordinal()).append(") ORDER BY ").append(Field.TIMESTAMP).append(" DESC LIMIT 1)"); ContentValues values = new ContentValues(); //this marks the timeline as the latest event for this contact. this is normally set in addTimeline //methods for the added event after resetting previous latest activities. values.put(Field.LATEST_CONTACT_STATUS.toString(), LATEST_STATUS_FOR_ALL | LATEST_STATUS_FOR_TYPE); writeableDb.update(TABLE_NAME, values, StringBufferPool.toStringThenRelease(where2), null); } } /** * This method returns the total number of timeline entries * for a particular contact. * * @param localContactId Given contact ID. * @param readableDb Readable SQLite database. * @return Timeline entry count or -1 incase of error. */ public static final int getTimelineEntriesCount(Long localContactId, SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "getTimelineEntriesCount()"); if (localContactId == null) { LogUtils.logE("getTimelineEntriesCount() localContactId is NULL"); return -1; } int timelineEntryCount = -1; Cursor cursor = null; try { // Get the count of Timeline entries marked as latest for the localcontactId. final StringBuffer latestTimelineQuery = StringBufferPool.getStringBuffer(); latestTimelineQuery.append("SELECT COUNT(*) ").append(" FROM ").append(TABLE_NAME) .append(" WHERE (").append(Field.FLAG).append("&").append(ActivityItem.TIMELINE_ITEM) .append(") AND ").append(Field.LOCAL_CONTACT_ID).append("=").append(localContactId) .append(" AND ").append(Field.LATEST_CONTACT_STATUS).append(" >0"); cursor = readableDb.rawQuery( StringBufferPool.toStringThenRelease(latestTimelineQuery), null); if (cursor != null) { if (cursor.moveToFirst()) { if (!cursor.isNull(0)) { timelineEntryCount = cursor.getInt(0); } } } } finally { CloseUtils.close(cursor); } return timelineEntryCount; } /** * This method updates the timeline entry for changed number. * * @param localContactId Given contact ID. * @param number The old number whose entry needs to be updated. * @param writeableDb Writable SQLite database. */ public static void updateTimeLineEntryForContact(Long localContactId, String number, SQLiteDatabase writableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "updateTimeLineEntryForContact()"); if (localContactId == null) { LogUtils.logE("updateTimeLineEntryForContact() localContactId is NULL"); return; } try { // Update the Timeline Entry to make localContactId, contactId NULL String[] args = { localContactId.toString(), number,localContactId.toString(), number }; final StringBuffer query = StringBufferPool.getStringBuffer(); query.append("UPDATE ").append(TABLE_NAME).append(" SET ") .append(Field.LOCAL_CONTACT_ID).append("=NULL, ") .append(Field.CONTACT_ID).append("=NULL, ").append(Field.CONTACT_NAME) .append("=").append(Field.CONTACT_ADDRESS).append(" WHERE ") .append(Field.LOCAL_CONTACT_ID).append("=? AND (").append(Field.FLAG) .append("&").append(ActivityItem.TIMELINE_ITEM).append(") AND ") .append(Field.CONTACT_ADDRESS).append("=?") .append(" and not exists (select * from ") .append(ContactDetailsTable.TABLE_NAME) .append(" where ") .append(ContactDetailsTable.Field.LOCALCONTACTID) .append("=? and ") .append(ContactDetailsTable.Field.STRINGVAL) .append("=?)"); writableDb.execSQL(StringBufferPool.toStringThenRelease(query), args); } catch (SQLException e) { LogUtils.logE("ActivitiesTable.updateTimeLineEntryForContact() " + "Unable to update Activities table", e); } } /** * This method updates the latest contact status entry for a timeline * entry identified by localContactId and timestamp value. * * @param localContactId Given contact ID. * @param latestContactStatus latest contact status. * @param timeStamp the timeStamp value for a timeline. * @param whereClause whereclause for update query. * @param writeableDb Writable SQLite database. */ public static void updateTimeLineStatusEntryForContact(Long localContactId, int latestContactStatus, Long timeStamp, String whereClause, SQLiteDatabase writableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "updateTimeLineStatusEntryForContact()"); if (localContactId == null) { LogUtils.logE("updateTimeLineStatusEntryForContact()" +" localContactId is NULL"); return; } try { String[] args = { localContactId.toString(), timeStamp.toString() }; final StringBuffer query = StringBufferPool.getStringBuffer(); query.append("UPDATE ").append(TABLE_NAME).append(" SET ") .append(Field.LATEST_CONTACT_STATUS).append("=") .append(latestContactStatus +" WHERE ").append(Field.LOCAL_CONTACT_ID) .append("=? AND (").append(Field.FLAG).append("&") .append(ActivityItem.TIMELINE_ITEM).append(") AND ") .append(Field.TIMESTAMP).append("=?"); if (whereClause != null) { query.append(whereClause); } writableDb.execSQL(StringBufferPool.toStringThenRelease(query), args); } catch (SQLException e) { LogUtils.logE("ActivitiesTable.updateTimeLineStatusEntryForContact() " + "Unable to update Activities table", e); } } /** * Fetches timeline events for a specific contact identified by local * contact ID in chronological order. * @param localContactId The local contact ID. * @param readableDb Readable SQLite database. * @return The cursor that can be read using * {@link #getTimelineData(Cursor)}. */ public static Cursor fetchTimelineEventsForContactById( final Long localContactId, final SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "fetchTimelineEventsForContact()"); Cursor cursor = null; try { final StringBuffer query = StringBufferPool.getStringBuffer(); query.append("SELECT ").append(Field.LOCAL_ACTIVITY_ID).append(",") .append(Field.TIMESTAMP).append(",").append(Field.CONTACT_NAME).append(",") .append(Field.CONTACT_AVATAR_URL).append(",").append(Field.LOCAL_CONTACT_ID) .append(",").append(Field.TITLE).append(",").append(Field.DESCRIPTION).append(",") .append(Field.CONTACT_NETWORK).append(",").append(Field.NATIVE_ITEM_TYPE).append(",") .append(Field.NATIVE_ITEM_ID).append(",").append(Field.TYPE).append(",") .append(Field.CONTACT_ID).append(",").append(Field.USER_ID).append(",") .append(Field.NATIVE_THREAD_ID).append(",").append(Field.CONTACT_ADDRESS).append(",") .append(Field.INCOMING).append(" FROM ").append(TABLE_NAME).append(" WHERE (") .append(Field.FLAG).append("&").append(ActivityItem.TIMELINE_ITEM).append(") AND ") .append(Field.LOCAL_CONTACT_ID).append("=").append(localContactId) .append(" ORDER BY ").append(Field.TIMESTAMP).append(" DESC"); cursor = readableDb.rawQuery(StringBufferPool.toStringThenRelease(query), null); } catch (SQLiteException e) { LogUtils.logE("ActivitiesTable.fetchTimelineEventsForContactById() " + "Unable to fetch timeline event for contact", e); } return cursor; } /** * This function separates the timeline. * entries of phone number and chat. * @param cursor pointing to the databases. * @param writeableDb The database * @param localContactId The localcontactId of the contact * @param oldPhoneNumber The old phone number to be changed */ public static void separateTimeLineEntries(final Cursor cursor , final SQLiteDatabase writeableDb , final Long localContactId , final String oldPhoneNumber) { // Split the latest timeline entries from the previous same localcontactId. if (cursor != null && cursor.getCount() > 1) { TimelineSummaryItem item = null; boolean isLatestTimelinePreferred = false; boolean isLatestTimeline = false; boolean isFirstRun = true; while (cursor.moveToNext()) { item = getTimelineData(cursor); if (item != null && item.mLocalContactId != null) { /** Debug added to catch bug causing PAND-2331. **/ LogUtils.logD("ActivitiesTable.updateTimelineContactData() " + "Neither mContactAddress[" + item.mContactAddress + "] mTimestamp[" + item.mTimestamp + "] should be NULL"); int latestContactStatus = LATEST_STATUS_FOR_ALL | LATEST_STATUS_FOR_TYPE; // Update the LatestContactStatus for Latest Timeline // Actually for chat timelines this item.mContactAddress will be null, // and it makes no sense to update it as well. if (item.mContactAddress != null) { if (item.mContactAddress.equals(oldPhoneNumber)) { if(isLatestTimelinePreferred) { latestContactStatus = 0; } final String whereClause = " AND " + Field.CONTACT_ADDRESS + "='" + oldPhoneNumber + "'"; updateTimeLineStatusEntryForContact(localContactId, latestContactStatus, item.mTimestamp, whereClause, writeableDb); isLatestTimelinePreferred = true; } else { if (isLatestTimeline) { latestContactStatus = 0; } // Update the remaining timeline entries for entries // other than the number changed. final String whereClause = " AND " + Field.CONTACT_ADDRESS + "!='" + oldPhoneNumber + "'"; updateTimeLineStatusEntryForContact(localContactId, latestContactStatus, item.mTimestamp, whereClause, writeableDb); isLatestTimeline = true; } } else { if (isFirstRun) { updateTimeLineStatusEntryForContact(localContactId, latestContactStatus, item.mTimestamp, null, writeableDb); isFirstRun = false; } } } } } } /** * Merges the entries when new number is added to existing contact. * Merges the chat and the phone messages entries present in Activities table. * @param cursor Cursor pointing to the Activities table. * @param writeableDb The database intance * @param localContactId The unique id associated with contact. */ public static void mergeTimeLineEntries( final Cursor cursor, final SQLiteDatabase writeableDb, final Long localContactId) { if (cursor != null && cursor.getCount() > 0) { cursor.moveToFirst(); int firstItemType = getTimelineData(cursor).mNativeItemType; TimelineSummaryItem timelineItem = null; // Skip the first latest timeline Entry and update the remaining. while (cursor.moveToNext()) { timelineItem = getTimelineData(cursor); if (timelineItem != null) { int latestContactStatus = getLatestContactStatusForContact(localContactId, timelineItem.mTimestamp, writeableDb); // Adjust the value for the former latest entry. // If it has been the same item type we remove both bits to hide it from // the time line. If the type differs we only remove the first bit so it // still visible on the type-specific timeline filter. if ((latestContactStatus & LATEST_STATUS_FOR_ALL) != 0) { if (firstItemType == timelineItem.mNativeItemType) { latestContactStatus &= ~(LATEST_STATUS_FOR_ALL | LATEST_STATUS_FOR_TYPE); } else { latestContactStatus &= ~LATEST_STATUS_FOR_ALL; } updateTimeLineStatusEntryForContact(localContactId, latestContactStatus, timelineItem.mTimestamp, null, writeableDb); } } } } } /** * This method gets the latest contact status entry for a timeline * entry identified by localContactId and timestamp value. * * @param localContactId Given contact ID. * @param timeStamp the timeStamp value for a timeline. * @param readableDb Readable SQLite database. */ private static int getLatestContactStatusForContact(Long localContactId, Long timeStamp, SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "getLatestContactStatusForContact()"); int localContactStatus = 0; if (localContactId == null) { LogUtils.logE("getLatestContactStatusForContact()" +" localContactId is NULL"); return localContactStatus; } Cursor cursor = null; try { String[] args = { localContactId.toString(), timeStamp.toString() }; final StringBuffer query = StringBufferPool.getStringBuffer(); query.append("SELECT ").append(Field.LATEST_CONTACT_STATUS) .append(" FROM ").append(TABLE_NAME) .append(" WHERE ").append(Field.LOCAL_CONTACT_ID) .append("=? AND (").append(Field.FLAG).append("&") .append(ActivityItem.TIMELINE_ITEM).append(") AND ") .append(Field.TIMESTAMP).append("=?"); cursor = readableDb.rawQuery(StringBufferPool.toStringThenRelease(query), args); if(cursor != null && cursor.moveToFirst()) { localContactStatus = cursor.getInt(0); } } catch (SQLException e) { LogUtils.logE("ActivitiesTable.getLatestContactStatusForContact() " + "Unable to fetch latestcontactstatus from Activities table", e); } finally { CloseUtils.close(cursor); } return localContactStatus; } /** * This method updates the timeline event. for the contact for the provided. * Phone number.Actually merges. the different entries into one. * * @param oldPhoneNumber * Phone number for which timeline entries need to be updated. * @param localContactId * Given contact. * @param writeableDb * Writable SQLite database. */ public static void updateTimelineForPhoneNumberChange( final String oldPhoneNumber, final Long localContactId, final SQLiteDatabase writeableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "updateTimelineForPhoneNumberChange()"); if (localContactId == null) { LogUtils.logE("updateTimelineForPhoneNumberChange localContactId is NULL"); return; } Cursor cursor = null; try { cursor = fetchTimelineEventsForContactById( localContactId, writeableDb); // Merge the different timeline entries for same localcontactId. // merge=true means the new number is added to contact&& timelineEntryCount > 1 mergeTimeLineEntries(cursor, writeableDb, localContactId); updateTimeLineEntryForContact( localContactId, oldPhoneNumber, writeableDb); } finally { CloseUtils.close(cursor); } } /** * This method updates the timeline.Separates * the deleted phone number entry from the other chat entries. * @param oldPhoneNumber Phone number for which timeline entries * need to be updated. * @param localContactId Given contact * @param writeableDb Writable SQLite database. * */ public static void updateTimelineForPhoneNumberDeletion( final String oldPhoneNumber, final Long localContactId, final SQLiteDatabase writeableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "updateTimelineForPhoneNumberDeletion()"); DatabaseHelper.trace(false, "DatabaseHelper." + "updateTimelineForPhoneNumberDeletion()"); if (localContactId == null) { LogUtils.logE("updateTimelineForPhoneNumberDeletion" + " localContactId is NULL"); return; } //One Use case not covered:-Suppose user enters one phone number to contact //But contact has not recieved any message/call from that number //Then no need to separate anything.That check can be made by seeing the values in //Activity table. and doing nothing as there wont be any separate entry for the number. Cursor cursor = null; try { cursor = fetchTimelineEventsForContactById( localContactId, writeableDb); separateTimeLineEntries( cursor, writeableDb, localContactId, oldPhoneNumber); updateTimeLineEntryForContact( localContactId, oldPhoneNumber, writeableDb); } finally { CloseUtils.close(cursor); } } /** * Fetches the Timelinedata with the given contactNumber. Returns the * TimelineSummaryItem with the latest data from Activities Table. * * @param contactNumber contactNumber whose TimelineSummary is needed. * @param readableDb Readable SQLite database. */ public static TimelineSummaryItem fetchTimelineSummaryFromContactNumber( final String contactNumber, final SQLiteDatabase readableDb) { DatabaseHelper.trace(false, "DatabaseHelper." + "fetchTimelineSummaryFromContactNumber()"); Cursor cursor = null; try { StringBuilder query = new StringBuilder("SELECT * FROM ") .append(TABLE_NAME); query.append(" WHERE ").append(Field.CONTACT_ADDRESS).append(" = ") .append("\"").append(contactNumber).append("\""); cursor = readableDb.rawQuery(query.toString(), null); if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0)) { return getTimelineData(cursor); } else { return null; } } finally { CloseUtils.close(cursor); } } }