/* * 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.engine.activities; import java.text.DateFormat; import java.util.ArrayList; import java.util.Date; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import com.vodafone360.people.database.DatabaseHelper; import com.vodafone360.people.database.tables.ActivitiesTable; import com.vodafone360.people.database.tables.StateTable; import com.vodafone360.people.database.tables.ActivitiesTable.TimelineSummaryItem; import com.vodafone360.people.datatypes.ActivityItem; import com.vodafone360.people.datatypes.Contact; import com.vodafone360.people.datatypes.ContactDetail; import com.vodafone360.people.datatypes.VCardHelper; import com.vodafone360.people.engine.activities.ActivitiesEngine.ISyncHelper; import com.vodafone360.people.service.ServiceStatus; import com.vodafone360.people.service.ServiceUiRequest; import com.vodafone360.people.utils.LogUtils; import com.vodafone360.people.utils.VersionUtils; /** * Fetches SMS/MMS log events from the Native message log. These are treated as * Activities and displayed in Timeline UI. */ public class FetchSmsLogEvents implements ISyncHelper { protected static final Uri SMS_CONTENT_URI = Uri.parse("content://sms"); /** * The maximum number of times the ISyncHelper should be called on this * sync (i.e. worker thread run loop). */ private static final int MAX_PAGES_TO_LOAD_AT_ONCE = 10; /** * The maximum number of Cursor rows that should be parsed on the current * run (i.e. while loop). */ private static final int MAX_ITEMS_PER_PAGE = 2; private static final int MAX_ITEMS_TO_WRITE = 10; private static final int MAX_DESC_LENGTH = 160; private static final String SMS_SORT_ORDER = "date DESC"; private static final String[] SMS_PROJECTION = new String[] { "_id", "date", "address", "subject", "body", "type", "thread_id" }; private static final int COLUMN_SMS_ID = 0; private static final int COLUMN_SMS_DATE = 1; private static final int COLUMN_SMS_ADDRESS = 2; private static final int COLUMN_SMS_SUBJECT = 3; private static final int COLUMN_SMS_BODY = 4; private static final int COLUMN_SMS_TYPE = 5; private static final int COLUMN_SMS_THREAD_ID = 6; private static final int MESSAGE_TYPE_INBOX = 1; private static final int MESSAGE_TYPE_SENT = 2; /** * Internal states for message log sync: Idle,, fetching SMS events, * fetching MMS events. */ private enum InternalState { IDLE, FETCHING_SMS_NEXT_PAGE, FETCHING_MMS_NEXT_PAGE } private Context mContext; private ActivitiesEngine mEngine; private DatabaseHelper mDb; private ContentResolver mCr; private InternalState mInternalState; private Cursor mSmsCursor; private Cursor mMmsCursor; private ArrayList<TimelineSummaryItem> mSyncItemList = new ArrayList<TimelineSummaryItem>(); /** * The result of fetching the timelines: ERROR_NOT_READY, SUCCESS (no change), * UPDATED_TIMELINES_FROM_NATIVE (DB changed). */ private ServiceStatus mStatus = ServiceStatus.SUCCESS; /** * is true if newer events need to be loaded, false - if older */ private boolean mRefresh; /** * the number of pages have been read */ private int mPageCount; /** * the current oldest message time */ private long mOldestMessage; /** * the current newest message time */ private long mNewestMessage; /** * Constructor. * * @param context Context - actually RemoteServe's Context. * @param engine Handle to ActivitiesEngine. * @param db Handle to DatabaseHelper. * @param refresh - true if we need to fetch new sms, false if older. */ FetchSmsLogEvents(Context context, ActivitiesEngine engine, DatabaseHelper db, boolean refresh) { mContext = context; mEngine = engine; mDb = db; mCr = mContext.getContentResolver(); mInternalState = InternalState.IDLE; mRefresh = refresh; } /** * Drive internal state machine, either start call log sync, fetch next page * of SMS items, next page of MMS items or complete call-log sync operation. */ @Override public void run() { switch (mInternalState) { case IDLE: startSyncSms(); break; case FETCHING_SMS_NEXT_PAGE: syncNextSmsPage(); break; case FETCHING_MMS_NEXT_PAGE: syncNextMmsPage(); break; default: mStatus = ServiceStatus.ERROR_NOT_READY; complete(mStatus); break; } } /** * /** Completion of fetch from Native message log. Notify ActivitiesEngine * that message-log sync. has completed. Close Cursors. * * @param status ServiceStatus containing result of sync. */ private void complete(ServiceStatus status) { mEngine.onSyncHelperComplete(status); cancel(); if (mMmsCursor != null) { mMmsCursor.close(); mMmsCursor = null; } if (mSmsCursor != null) { mSmsCursor.close(); mSmsCursor = null; } } /** * Start sync of SMS message events. Use Timeline last update time-stamp to * ensure we only fetch 'new' events. */ private void startSyncSms() { mNewestMessage = StateTable.fetchLatestSmsTime(mDb.getReadableDatabase()); mOldestMessage = StateTable.fetchOldestSmsTime(mDb.getReadableDatabase()); // at 1st sync the StateTable contains no value: so for "refresh" set // value to 0, for "more" to current time if (mOldestMessage == 0) { mOldestMessage = System.currentTimeMillis(); StateTable.modifyOldestSmsTime(mOldestMessage, mDb.getWritableDatabase()); } String whereClause = mRefresh ? "date > " + mNewestMessage : "date < " + mOldestMessage; mSmsCursor = mCr.query(SMS_CONTENT_URI, SMS_PROJECTION, whereClause, null, SMS_SORT_ORDER); mInternalState = InternalState.FETCHING_SMS_NEXT_PAGE; syncNextSmsPage(); } /** * Start sync. of MMS message events. Use Timeline last update time-stamp to * ensure we only fetch 'new' events. */ private void startSyncMms() { mNewestMessage = StateTable.fetchLatestMmsTime(mDb.getReadableDatabase()); mOldestMessage = StateTable.fetchOldestMmsTime(mDb.getReadableDatabase()); // at 1st sync the StateTable contains no value: so for "refresh" set // value to 0, for "more" to current time if (mOldestMessage == 0) { mOldestMessage = System.currentTimeMillis(); StateTable.modifyOldestMmsTime(mOldestMessage, mDb.getWritableDatabase()); } mMmsCursor = MmsDecoder.fetchMmsListCursor(mCr, mRefresh, mNewestMessage, mOldestMessage); mInternalState = InternalState.FETCHING_MMS_NEXT_PAGE; syncNextMmsPage(); } /** * Sync. next page of SMS events (current page size is 2). */ private void syncNextSmsPage() { if (mSmsCursor.isAfterLast()) { mSmsCursor.close(); mSmsCursor = null; startSyncMms(); return; } boolean finished = false; if (mPageCount < MAX_PAGES_TO_LOAD_AT_ONCE) { int count = 0; int id = 0; long timestamp = 0; while (count < MAX_ITEMS_PER_PAGE && mSmsCursor.moveToNext()) { id = mSmsCursor.getInt(COLUMN_SMS_ID); timestamp = mSmsCursor.getLong(COLUMN_SMS_DATE); if (mRefresh) { if (timestamp < mNewestMessage) { finished = true; break; } } else { if (timestamp > mOldestMessage) { finished = true; break; } } addSmsData(id); count++; } mPageCount++; if ((count == MAX_ITEMS_PER_PAGE && mSyncItemList.size() == MAX_ITEMS_TO_WRITE) || (count < MAX_ITEMS_PER_PAGE) || (mPageCount == MAX_PAGES_TO_LOAD_AT_ONCE)) { ServiceStatus status = mDb.addTimelineEvents(mSyncItemList, false); updateTimestamps(); saveTimeStampSms(); mSyncItemList.clear(); if (ServiceStatus.SUCCESS != status) { mStatus = status; complete(mStatus); return; } mEngine.fireNewState(ServiceUiRequest.DATABASE_CHANGED_EVENT, new Bundle()); } } else { finished = true; } if (finished) { saveTimeStampSms(); mPageCount = 0; mSmsCursor.close(); mSmsCursor = null; startSyncMms(); } } /** * This method goes through the event list and updates the current newest * and oldest message timestamps. */ private void updateTimestamps() { if (mSyncItemList.size() > 0) { long max = ((TimelineSummaryItem)mSyncItemList.get(0)).mTimestamp; long min = max; for (TimelineSummaryItem item : mSyncItemList) { if (item.mTimestamp > max) { max = item.mTimestamp; } if (item.mTimestamp < min) { min = item.mTimestamp; } } if (mNewestMessage < max) { mNewestMessage = max; } if (mOldestMessage > min) { mOldestMessage = min; } } } /** * This method updates the newest and oldest MMS timestamps in the database. */ private void saveTimeStampMms() { long saved = StateTable.fetchOldestMmsTime(mDb.getReadableDatabase()); if (mOldestMessage < saved) { StateTable.modifyOldestMmsTime(mOldestMessage, mDb.getWritableDatabase()); LogUtils.logD("FetchMMSEvents saveTimestamp: oldest timeline update set to = " + mOldestMessage); mStatus = ServiceStatus.UPDATED_TIMELINES_FROM_NATIVE; } saved = StateTable.fetchLatestMmsTime(mDb.getReadableDatabase()); if (mNewestMessage > saved) { StateTable.modifyLatestMmsTime(mNewestMessage, mDb.getWritableDatabase()); LogUtils.logD("FetchMMSEvents saveTimestamp: newest timeline update set to = " + mNewestMessage); mStatus = ServiceStatus.UPDATED_TIMELINES_FROM_NATIVE; } } /** * This method updates the newest and oldest SMS timestamps in the database. */ private void saveTimeStampSms() { long saved = StateTable.fetchOldestSmsTime(mDb.getReadableDatabase()); if (mOldestMessage < saved) { StateTable.modifyOldestSmsTime(mOldestMessage, mDb.getWritableDatabase()); LogUtils.logD("FetchSMSEvents saveTimestamp: oldest timeline update set to = " + mOldestMessage); mStatus = ServiceStatus.UPDATED_TIMELINES_FROM_NATIVE; } saved = StateTable.fetchLatestSmsTime(mDb.getReadableDatabase()); if (mNewestMessage > saved) { if (VersionUtils.isHtcSenseDevice(mContext)) { /* * There is apparently a sms timestamp issue on some of these devices * where received messages use the network time and the sent messages the device time. * This can cause huge time difference and storing the exact timestamp is not safe enough. * The quick and dirty hack below subtracts 12 hours in milliseconds so that even if the device is * traveling or messages are received from abroad, they should be still considered * for an import in the people client. However, this won't fix the sorting of the messages * since it is a platform issue: the sorting is also wrong within the native message app. */ mNewestMessage -= 12*60*60*1000; } StateTable.modifyLatestSmsTime(mNewestMessage, mDb.getWritableDatabase()); LogUtils.logD("FetchSMSEvents saveTimestamp: newest timeline update set to = " + mNewestMessage); mStatus = ServiceStatus.UPDATED_TIMELINES_FROM_NATIVE; } } /** * Sync. next page of MMS events (current page size is 2). */ private void syncNextMmsPage() { if (mMmsCursor.isAfterLast()) { complete(mStatus); return; } boolean finished = false; if (mPageCount < MAX_PAGES_TO_LOAD_AT_ONCE) { int count = 0; while (count < MAX_ITEMS_PER_PAGE && mMmsCursor.moveToNext()) { final long timestamp = MmsDecoder.getTimestamp(mMmsCursor); if (mRefresh) { if (timestamp < mNewestMessage) { finished = true; break; } } else { if (timestamp > mOldestMessage) { finished = true; break; } } TimelineSummaryItem item = new TimelineSummaryItem(); if (MmsDecoder.getMmsData(mContext, mCr, mMmsCursor, item, mDb, MAX_DESC_LENGTH)) { LogUtils.logD("FetchSmsLogEvents.syncNextMmsPage(): id = " + item.mNativeItemId + ", name = " + item.mContactName + ", date = " + item.mTimestamp + ", title = " + item.mTitle + ", desc = " + item.mDescription + "\n"); mSyncItemList.add(item); } count++; } mPageCount++; if ((count == MAX_ITEMS_PER_PAGE && mSyncItemList.size() == MAX_ITEMS_TO_WRITE) || (count < MAX_ITEMS_PER_PAGE) || (mPageCount == MAX_PAGES_TO_LOAD_AT_ONCE)) { ServiceStatus status = mDb.addTimelineEvents(mSyncItemList, false); updateTimestamps(); mSyncItemList.clear(); if (ServiceStatus.SUCCESS != status) { complete(mStatus); return; } mEngine.fireNewState(ServiceUiRequest.DATABASE_CHANGED_EVENT, new Bundle()); } } else { finished = true; } if (finished) { saveTimeStampMms(); complete(mStatus); } } /** * Create TimelineSummaryItem from Native message-log item. * * @param id ID of item from Native log. */ private void addSmsData(int id) { ActivityItem.Type type = nativeToNpTypeConvert(mSmsCursor.getInt(COLUMN_SMS_TYPE)); if (type == null) { return; } TimelineSummaryItem item = new TimelineSummaryItem(); String address = null; /* Francisco: Unknown contact SMS sending bug resolved here * I am keeping previous case SN_MESSAGE_RECEIVED besides MESSAGE_SMS_RECEIVED just to be safe. */ if (type == ActivityItem.Type.SN_MESSAGE_RECEIVED || type == ActivityItem.Type.MESSAGE_SMS_RECEIVED) { item.mIncoming = TimelineSummaryItem.Type.INCOMING; } else { item.mIncoming = TimelineSummaryItem.Type.OUTGOING; } item.mNativeItemId = id; item.mNativeItemType = ActivitiesTable.TimelineNativeTypes.SmsLog.ordinal(); item.mType = type; item.mTimestamp = mSmsCursor.getLong(COLUMN_SMS_DATE); item.mTitle = DateFormat.getDateInstance().format(new Date(item.mTimestamp)); item.mNativeThreadId = mSmsCursor.getInt(COLUMN_SMS_THREAD_ID); item.mDescription = null; if (!mSmsCursor.isNull(COLUMN_SMS_SUBJECT)) { item.mDescription = mSmsCursor.getString(COLUMN_SMS_SUBJECT); } if (item.mDescription == null || item.mDescription.length() == 0) { if (!mSmsCursor.isNull(COLUMN_SMS_BODY)) { item.mDescription = mSmsCursor.getString(COLUMN_SMS_BODY); } } if (!mSmsCursor.isNull(COLUMN_SMS_ADDRESS)) { address = mSmsCursor.getString(COLUMN_SMS_ADDRESS); } item.mContactName = address; item.mContactAddress = address; Contact c = new Contact(); ContactDetail phoneDetail = new ContactDetail(); ServiceStatus status = mDb.fetchContactInfo(address, c, phoneDetail); if (ServiceStatus.SUCCESS == status) { item.mLocalContactId = c.localContactID; item.mContactId = c.contactID; item.mUserId = c.userID; item.mContactName = null; for (ContactDetail d : c.details) { switch (d.key) { case VCARD_NAME: final VCardHelper.Name name = d.getName(); if (name != null) { item.mContactName = d.getName().toString(); } break; case VCARD_IMADDRESS: item.mContactNetwork = d.alt; break; default: // do nothing break; } } } if (item.mContactId == null) { LogUtils.logI("FetchSmsLogEvents.addSmsData: id " + item.mNativeItemId + ", time " + item.mTitle + ", description " + item.mDescription); } else { LogUtils.logI("FetchSmsLogEvents.addSmsData: id " + item.mNativeItemId + ", name = " + item.mContactName + ", time " + item.mTitle + ", description " + item.mDescription); } mSyncItemList.add(item); } /** * Convert Native message type (Inbox, Sent) to corresponding ActivityItem * type. * * @param type Native message type. * @return ActivityItem type. */ private ActivityItem.Type nativeToNpTypeConvert(int type) { switch (type) { case MESSAGE_TYPE_INBOX: return ActivityItem.Type.MESSAGE_SMS_RECEIVED; case MESSAGE_TYPE_SENT: return ActivityItem.Type.MESSAGE_SMS_SENT; default: return null; } } /** * Cancel message log sync. */ @Override public void cancel() { mInternalState = InternalState.IDLE; } }