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