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