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.security.InvalidParameterException;
  import java.util.ArrayList;
  import java.util.Hashtable;
  import java.util.List;
  import java.util.Map;
  
  import android.content.Context;
  import android.os.Bundle;
  
  import com.vodafone360.people.ApplicationCache;
  import com.vodafone360.people.database.DatabaseHelper;
  import com.vodafone360.people.database.tables.ActivitiesTable;
  import com.vodafone360.people.database.tables.StateTable;
  import com.vodafone360.people.datatypes.ActivityItem;
  import com.vodafone360.people.datatypes.BaseDataType;
  import com.vodafone360.people.datatypes.PushEvent;
  import com.vodafone360.people.engine.BaseEngine;
  import com.vodafone360.people.engine.EngineManager;
  import com.vodafone360.people.engine.IEngineEventCallback;
  import com.vodafone360.people.engine.EngineManager.EngineId;
  import com.vodafone360.people.engine.contactsync.ContactSyncEngine;
  import com.vodafone360.people.engine.contactsync.ContactSyncEngine.IContactSyncObserver;
  import com.vodafone360.people.engine.login.LoginEngine.ILoginEventsListener;
  import com.vodafone360.people.service.ServiceStatus;
  import com.vodafone360.people.service.ServiceUiRequest;
  import com.vodafone360.people.service.agent.NetworkAgent;
  import com.vodafone360.people.service.agent.UiAgent;
  import com.vodafone360.people.service.io.ResponseQueue.DecodedResponse;
  import com.vodafone360.people.service.io.api.Activities;
  import com.vodafone360.people.service.io.rpg.PushMessageTypes;
  import com.vodafone360.people.utils.LogUtils;
  
  /**
   * Engine response for handling of Activities (i.e Timeline and Status events).
   * It also interfaces to the native Call and Message logs to allow these events
   * to be handled within the People Client's Timeline.
   */
  public class ActivitiesEngine extends BaseEngine implements IContactSyncObserver,
          ILoginEventsListener {
  
      /**
       * below are the constants to communicate the engine state states to UI: 3
       * buttons (update statuses, load older statuses, older timelines)
       */
      private enum ActivitiesState {
          UPDATING_STATUSES, FETCHING_OLDER_STATUSES, FETCHING_OLDER_TIMELINE
      }
  
      /** filter definitions */
      private static final String FILTER_UPDATED = "f.updated";
  
      private static final String FILTER_GT = ">";
  
      /** Identifier for filtering against local ids (LIDs) */
      private static final String FILTER_LIDS = "lids";
  
      /** maximum number of identities fetched at the same time */
      private static final String FILTER_NUM = "0-150";
  
      private static final String FILTER_SORT = "sort";
  
      private static final String FILTER_UPDATED_REV = "updated?rev";
  
      // filter out timeline events and only retrieve status activities
      private static final String FILTER_STATUS = "status";
  
      private static final String FILTER_TRUE = "true";
  
      private static final long MS_IN_SECOND = 1000L;
      /**
       * Interval of how often the Activities table should be cleaned up.
       * Currently a cleanup will take place once every day, or more often if the
       * client is unloaded from memory.
       **/
      private static final long ACTIVITES_CLEANUP_SEC = 24 * 60 * 60 * MS_IN_SECOND;
  
      private static final long READ_TIMELINES_TIMEOUT_MILLS = 0;
  
      private final static long WEEK_OLD_MILLIS = 7 * 24 * 60 * 60 * 1000;
  
      /** Timestamp for most recent Status event update */
      private long mLastStatusUpdated;
  
      /** Timestamp for oldest Status event update */
      private long mOldestStatusUpdated;
  
      /**
       * Time stamp for the next time the Activities table should be cleaned up.
       * This member variable acts as an in-memory throttle to prevent the
       * cleanup happening too often while the client in on screen (i.e.
       * avoid flickering), but to run at least once every ACTIVITES_CLEANUP_SEC
       * so long as the current process is alive.
       **/
      private long mNextCleanup = -1;
      
      private Context mContext;
  
      /** engine's current state **/
      private State mState = State.IDLE;
  
      private boolean mRequestActivitiesRequired;
  
      private final Hashtable<Integer, ActivitiesState> mActiveRequests = new Hashtable<Integer, ActivitiesState>();
  
      /**
       * Instance of TimelineEventWatcher which listens for native call and
       * message logs events that ActivitiesEngine should sync.
       */
      private TimelineEventWatcher mTimelineEventWatcher;
  
      private DatabaseHelper mDb;
  
      private final Object mQueueMutex = new Object();
  
      /**
       * TRUE if the timelines have been updated with the previous "show more"
       * call or NAB event - it is sort of a "cached" flag for the UI to display
       * the animated buttons in timeline screen correctly.
       */
      private boolean mTimelinesUpdated;
  
      /**
       * Definitions of Activities engines states; IDLE - engine is inactive
       * FETCH_STATUSES_FIRST_TIME: 1st time sync of statuses,
       * FETCH_CALLOG_FIRST_TIME: first time sync of phone calls, UPDATE_STATUSES:
       * updating statuses from server (user has pressed the "refresh button" or
       * push event's triggered the sync), UPDATE_CALLOG_FROM_NATIVE: new timeline
       * item appeared on the phone, FETCH_OLDER_CALLLOG_FROM_NATIVE_DB: user has
       * pressed "more timelines" button, FETCH_SMS_FROM_NATIVE_DB: fetching
       * SMS/MMS
       */
      private enum State {
          IDLE,
          FETCH_STATUSES_FIRST_TIME,
          FETCH_CALLOG_FIRST_TIME,
          UPDATE_STATUSES,
          UPDATE_CALLOG_FROM_NATIVE,
          FETCH_OLDER_CALLLOG_FROM_NATIVE_DB,
          FETCH_SMS_FROM_NATIVE_DB
      }
  
      /**
       * Interface for Native call and message log sync classes.
       */
      static interface ISyncHelper {
          void run();
  
          void cancel();
      }
  
      /**
       * current processor (fetching timelines), is null if no action for
       * timelines is taken
       */
      private ISyncHelper mActiveSyncHelper;
  
      /**
       * mutex for thread synchronization
       */
      private final Object mMutex = new Object();
  
      /**
       * Constructor
       * 
       * @param context - valid context
       * @param eventCallback - callback object
       * @param db - DatabaseHepler object
       */
      public ActivitiesEngine(Context context, IEngineEventCallback eventCallback, DatabaseHelper db) {
          super(eventCallback);
          mEngineId = EngineId.ACTIVITIES_ENGINE;
          mDb = db;
          mContext = context;
          if (isContactSyncReady()) {
              EngineManager.getInstance().getContactSyncEngine().addEventCallback(this);
          }
          mTimelineEventWatcher = new TimelineEventWatcher(mContext, this);
      }
  
      /**
       * Return next run time for ActivitiesEngine. Determined by whether we have
       * a request we wish to issue, or there is a response that needs processing.
       */
      @Override
      public long getNextRunTime() {
          if (!isContactSyncReady()
                  || !EngineManager.getInstance().getContactSyncEngine().isFirstTimeSyncComplete()) {
              return -1;
          }
          if (mNextCleanup < System.currentTimeMillis()) {
              return 0;
          }
          if (isCommsResponseOutstanding()) {
              return 0;
          }
          if (isUiRequestOutstanding()) {
              return 0;
          }
          if (mRequestActivitiesRequired && checkConnectivity()) {
              return 0;
          }
          return getCurrentTimeout();
  
      }
  
      /**
       * onCreate. Instruct the timeline event watcher to start watching for
       * native events the Activities engine may need to handle (call and message
       * events).
       */
      @Override
      public void onCreate() {
          mTimelineEventWatcher.startWatching();
      }
  
      /**
       * On destruction of ActivitiesEngine stop the timeline event watcher.
       */
      @Override
      public void onDestroy() {
          mTimelineEventWatcher.stopWatching();
          if (isContactSyncReady()) {
              EngineManager.getInstance().getLoginEngine().removeListener(this);
          }
      }
  
      /** {@inheritDoc} */
      @Override
      protected void onRequestComplete() {
      }
  
      /**
       * Drive state machine for Activities sync. If we are currently fetching
       * native events run the appropriated ISyncHelper. Otherwise request
       * retrieval of Activities from Server.
       */
  
      @Override
      protected void onTimeoutEvent() {
          // run now
          LogUtils.logV("ActivitiesEngine onTimeoutEvent:" + mState);
          switch (mState) {
              case FETCH_CALLOG_FIRST_TIME:
              case FETCH_OLDER_CALLLOG_FROM_NATIVE_DB:
              case FETCH_SMS_FROM_NATIVE_DB:
              case UPDATE_CALLOG_FROM_NATIVE:
                  mActiveSyncHelper.run();
                  if (mActiveSyncHelper != null) {
                      // will be null when the last batch is read, so no need to
                      // fire timeout event - onSyncComplete sets it to null
                      setTimeout(READ_TIMELINES_TIMEOUT_MILLS);
                  }
                  break;
              case IDLE:
                  addStatusesSyncRequest();
                  break;
              default:
                  // do nothing
          }
      }
  
      /**
       * Handle response received from transport layer (via EngineManager)
       * 
       * @param resp Received Response item either a Status/Timeline related push
       *            message or a response to a get activities request.
       */
      @Override
      protected void processCommsResponse(DecodedResponse resp) {
          LogUtils.logD("ActivitiesEngine processCommsResponse");
          // handle push response
          if (resp.mReqId == 0 && resp.mDataTypes.size() > 0) {
              PushEvent evt = (PushEvent)resp.mDataTypes.get(0);
              handlePushRequest(evt.mMessageType);
          } else {
              dequeueRequest(resp.mReqId);
              handleGetActivitiesResponse(resp.mDataTypes);
          }
      }
  
      /**
       * Handle an outstanding UI request.
       */
      @Override
      protected void processUiRequest(ServiceUiRequest requestId, Object data) {
          LogUtils.logD("ActivityEngine processUiRequest:" + requestId);
          switch (requestId) {
              case UPDATE_STATUSES:
                  // this is full sync or push, or "refresh" button
                  requestStatusesFromServer(true);
                  break;
              case FETCH_STATUSES:
                  // "more" button
                  requestStatusesFromServer(false);
                  break;
              case FETCH_TIMELINES:
                  // "more" button - we only need time lines
                  enqueueRequest(ServiceUiRequest.FETCH_TIMELINES.ordinal(), ActivitiesState.FETCHING_OLDER_TIMELINE);
                  newState(State.FETCH_OLDER_CALLLOG_FROM_NATIVE_DB);
                  startCallLogSync(false);
                  break;
              case UPDATE_PHONE_CALLS:
                  // something on the NAB has changed
                  newState(State.UPDATE_CALLOG_FROM_NATIVE);
                  startCallLogSync(true);
                  break;
              case UPDATE_SMS:
                  newState(State.FETCH_SMS_FROM_NATIVE_DB);
                  startFetchingSMS();
                  break;
              default:
                  break;
          }
      }
  
      /**
       * ActivitiesEngine run implementation Processes a response if one is
       * available, Processes any events in engine's UI queue. Issues
       * get-activities request to server as part of sync.
       */
      @Override
      public void run() {
          LogUtils.logD("ActivityEngine run");
          processTimeout();
          if (mNextCleanup < System.currentTimeMillis()) {
              cleanDatabase();
              mNextCleanup = System.currentTimeMillis() + ACTIVITES_CLEANUP_SEC;
              LogUtils.logD("ActivityEngine.run() Clean database again at ["
                      + mNextCleanup + "]");
              return;
          }
          if (isCommsResponseOutstanding() && processCommsInQueue()) {
              return;
          }
          if (isUiRequestOutstanding()) {
              processUiQueue();
          }
          if (mRequestActivitiesRequired) {
              requestStatusesFromServer(true);
          }
      }
  
      /**
       * Start sync of SMSs from native data base
       */
      private void startFetchingSMS() {
          mActiveSyncHelper = new FetchSmsLogEvents(mContext, this, mDb, true);
          setTimeout(READ_TIMELINES_TIMEOUT_MILLS);
      }
  
      /**
       * Start sync of call log from native call log data base
       */
      private void startCallLogSync(boolean refresh) {
  
          mActiveSyncHelper = new FetchCallLogEvents(mContext, this, mDb, refresh);
          setTimeout(READ_TIMELINES_TIMEOUT_MILLS);
      }
  
      /**
       * This method is necessary for the tests. It catches the
       * InvalidParameterException coming from EngineManager.getInstance(), when
       * the instance is null
       * 
       * @return boolean - TRUE if ContactSyncEngine is not null
       */
      private boolean isContactSyncReady() {
          try {
              return (EngineManager.getInstance() != null)
                      && (EngineManager.getInstance().getContactSyncEngine() != null);
          } catch (InvalidParameterException ipe) {
              LogUtils.logE(ipe.toString());
              return false;
          }
  
      }
  
      /**
       * Request Activities (Status/Timeline) events from Server.
       * 
       * @param refresh boolean - is true when fetching latest statuses, false -
       *            when older
       */
      private void requestStatusesFromServer(boolean refresh) {
          if (!checkConnectivity()) {
              mRequestActivitiesRequired = true;
              return;
          }
          mRequestActivitiesRequired = false;
          if (!isContactSyncReady()
                  || !EngineManager.getInstance().getContactSyncEngine().isFirstTimeSyncComplete()) {
              // this method will then call completeUiRequest(status, null);
              onSyncHelperComplete(ServiceStatus.ERROR_NOT_READY);
              return;
          }
          mLastStatusUpdated = StateTable.fetchLatestStatusUpdateTime(mDb.getReadableDatabase());
          mOldestStatusUpdated = StateTable.fetchOldestStatusUpdate(mDb.getReadableDatabase());
  
          LogUtils.logD("ActivityEngine getActivites last update = " + mLastStatusUpdated);
  
          int reqId = Activities.getActivities(this, null, applyActivitiesFilter(refresh));
          if (reqId > 0) {
              setReqId(reqId);
              enqueueRequest(reqId, refresh ? ActivitiesState.UPDATING_STATUSES : ActivitiesState.FETCHING_OLDER_STATUSES);
  
              if (mLastStatusUpdated == 0) {
                  newState(State.FETCH_STATUSES_FIRST_TIME);
              } else {
                  newState(State.UPDATE_STATUSES);
              }
          }
      }
  
      private Map<String, List<String>> applyActivitiesFilter(boolean refresh) {
  
          Map<String, List<String>> filter = new Hashtable<String, List<String>>();
  
          // filter out types we're not interested in
          List<String> statusFilter = new ArrayList<String>();
          statusFilter.add(FILTER_TRUE);
          filter.put(FILTER_STATUS, statusFilter);
  
          if (mLastStatusUpdated > 0) {
              if (refresh) {
                  List<String> updateFilter = new ArrayList<String>();
                  LogUtils.logD("ActivityEngine TimeFilter newer= '" + FILTER_GT
                          + (mLastStatusUpdated / MS_IN_SECOND) + "'");
                  updateFilter.add(FILTER_GT + mLastStatusUpdated / MS_IN_SECOND);
                  filter.put(FILTER_UPDATED, updateFilter);
              } else {
                  List<String> updateFilter = new ArrayList<String>();
                  LogUtils.logD("ActivityEngine TimeFilter older= '" + FILTER_GT
                          + (mOldestStatusUpdated / MS_IN_SECOND) + "'");
                  updateFilter.add(FILTER_GT + mOldestStatusUpdated / MS_IN_SECOND);
                  filter.put(FILTER_UPDATED, updateFilter);
              }
          } else { // 1st time
              List<String> fNum = new ArrayList<String>();
              fNum.add(FILTER_NUM);
              filter.put(FILTER_LIDS, fNum);
              List<String> sort = new ArrayList<String>();
              sort.add(FILTER_UPDATED_REV);
              filter.put(FILTER_SORT, sort);
  
              mOldestStatusUpdated = (System.currentTimeMillis() - WEEK_OLD_MILLIS) / MS_IN_SECOND;
          }
          return filter;
      }
  
      /**
       * Trigger the ActivitiesTable cleanup.
       */
      private void cleanDatabase() {
          ActivitiesTable.cleanupActivityTable(mDb.getWritableDatabase());
      }
  
      /**
       * Handle Status or Timeline Activity change Push message
       * 
       * @param evt Push message type (Status change or Timeline change).
       */
      private void handlePushRequest(PushMessageTypes evt) {
          LogUtils.logD("ActivityEngine handlePushRequest");
          switch (evt) {
              case STATUS_ACTIVITY_CHANGE:
              case TIMELINE_ACTIVITY_CHANGE:
                  addUiRequestToQueue(ServiceUiRequest.UPDATE_STATUSES, null);
                  break;
              default:
                  // do nothing
          }
      }
  
      /**
       * Handle GetActivities response message received from Server
       * 
       * @param reqId Request ID contained in response. This should match an ID of
       *            a request we have issued to the Server.
       * @param data List array of ActivityItem items returned from Server.
       */
      private void handleGetActivitiesResponse(List<BaseDataType> data) {
          /** Array of Activities retrieved from Server. */
          ArrayList<ActivityItem> activityList = new ArrayList<ActivityItem>();
          ServiceStatus errorStatus = getResponseStatus(BaseDataType.ACTIVITY_ITEM_DATA_TYPE, data);
          LogUtils.logE("ActivityEngine.handleGetActivitiesResponse status from generic = "
                  + errorStatus);
          if (ServiceStatus.SUCCESS == errorStatus) {
              for (BaseDataType item : data) {
                  if (item.getType() == BaseDataType.ACTIVITY_ITEM_DATA_TYPE) {
                      activityList.add((ActivityItem)item);
                  } else {
                      LogUtils
                              .logE("ActivityEngine.handleGetActivitiesResponse will not handle strange type = "
                                      + item.getType());
                  }
              }
              errorStatus = updateDatabase(activityList);
              // we set timeout for the next execution
          }
          // this method will then call completeUiRequest(status, null);
          onSyncHelperComplete(errorStatus);
      }
  
      private ServiceStatus updateDatabase(ArrayList<ActivityItem> activityList) {
          ServiceStatus errorStatus = ServiceStatus.SUCCESS;
  
          // add retrieved items to Activities table in db
          removeDuplicates(activityList);
  
          // update the newest activity
          Long temp = findLastStatusUpdateTime(activityList);
          if (temp != Long.MIN_VALUE) {
              mLastStatusUpdated = temp;
          }
          if (activityList.size() > 0) {
              LogUtils.logD("ActivityEngine Added ActivityItems = " + activityList.size());
              // update database
              errorStatus = mDb.addActivities(activityList);
              if (errorStatus == ServiceStatus.SUCCESS) {
                  updateLatestStatusUpdateTime();
                  updateOldestStatusUpdateTime();
              }
          }
          return errorStatus;
      }
  
      private void updateLatestStatusUpdateTime() {
          long tluStatus = StateTable.fetchLatestStatusUpdateTime(mDb.getReadableDatabase());
          // modify the timelines dates
          if (mLastStatusUpdated > tluStatus) {
              StateTable.modifyLatestStatusUpdateTime(mLastStatusUpdated, mDb.getWritableDatabase());
              LogUtils.logD("ActivityEngine: last status update set to = " + mLastStatusUpdated);
          }
      }
  
      private void updateOldestStatusUpdateTime() {
          long tloStatus = StateTable.fetchLatestStatusUpdateTime(mDb.getReadableDatabase());
          // modify the timelines dates
          if (mOldestStatusUpdated < tloStatus) {
              StateTable.modifyOldestStatusTime(mOldestStatusUpdated, mDb.getWritableDatabase());
              LogUtils.logD("ActivityEngine: oldest status update set to = " + mOldestStatusUpdated);
          }
      }
  
      private Long findLastStatusUpdateTime(ArrayList<ActivityItem> activityList) {
          Long ret = Long.MIN_VALUE;
          for (ActivityItem ai : activityList) {
              if (ai.time != null && (ai.time.compareTo(ret) > 0)) {
                  ret = ai.time;
              }
          }
          return ret;
      }
  
      /**
       * Find oldest item in list of Activities retrieved from server.
       * 
       * @return Time-stamp of oldest event.
       */
      private Long findFirstStatusUpdateTime(ArrayList<ActivityItem> activityList) {
          Long ret = Long.MAX_VALUE;
          for (ActivityItem ai : activityList) {
              if (ai.time != null && ai.time.compareTo(ret) < 0) {
                  ret = ai.time;
              }
          }
          return ret;
      }
  
      /**
       * TODO: investigate why duplicates here can appear. this method might be
       * not necessary. Remove Activities from list of Activities with Activity
       * IDS matching ones we have already retrieved (i.e .duplicates).
       */
      private void removeDuplicates(ArrayList<ActivityItem> activityList) {
          if (activityList.size() == 0) {
              return;
          }
          int dupCount = 0;
          List<Long> actIdList = new ArrayList<Long>();
          mDb.fetchActivitiesIds(actIdList, findFirstStatusUpdateTime(activityList));
          for (int i = 0; i < activityList.size();) {
              boolean inc = true;
              Long id = activityList.get(i).activityId;
              if (id != null) {
                  for (Long l : actIdList) {
                      if (l.compareTo(id) == 0) {
                          activityList.remove(i);
                          inc = false;
                          dupCount++;
                          break;
                      }
                  }
              }
              if (inc) {
                  i++;
              }
          }
          LogUtils.logD("ActivityEngine removeDuplicates. Count dups = " + dupCount);
      }
  
      /**
       * Receive notification from ContactSyncEngine that a Contact sync has
       * completed. Start Activity sync if we have not previously synced.
       */
      @Override
      public void onSyncComplete(ServiceStatus status) {
          LogUtils.logD("ActivityEngine onSyncComplete, time:" + mLastStatusUpdated + ", state="
                  + mState);
          // fire off background grab of activities
          if (ServiceStatus.SUCCESS == status && (mLastStatusUpdated == 0) && (mState == State.IDLE)) {
              addStatusesSyncRequest();
              LogUtils.logD("ActivityEngine onSyncComplete FULL_SYNC_FIRST_TIME.");
          }
      }
  
      /** {@inheritDoc} */
      @Override
      public void onContactSyncStateChange(ContactSyncEngine.Mode mode,
              ContactSyncEngine.State oldState, ContactSyncEngine.State newState) {
          LogUtils.logD("ActivityEngine onContactSyncStateChange called.");
      }
  
      /** {@inheritDoc} */
      @Override
      public void onProgressEvent(ContactSyncEngine.State currentState, int percent) {
          LogUtils.logD("ActivityEngine onProgressEvent");
      }
  
      /**
       * Get current connectivity state from the NetworkAgent. If not connected
       * completed UI request with COMMs error.
       * 
       * @return true NetworkAgent reports we are connected, false otherwise.
       */
      private boolean checkConnectivity() {
          if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
              completeUiRequest(ServiceStatus.ERROR_COMMS, null);
              return false;
          }
          return true;
      }
  
      /**
       * Function called when helper classes ends their work. Currently supported
       * helpers are FetchCallLogEvents and FetchSmsLogEvents
       * 
       * @param status - ServiceStatus of completed operation.
       */
      protected void onSyncHelperComplete(ServiceStatus status) {
          if (ServiceStatus.UPDATED_TIMELINES_FROM_NATIVE == status) {
              mTimelinesUpdated = true;
          }
          switch (mState) {
              case FETCH_STATUSES_FIRST_TIME:
                  // 1st/full sync
                  newState(State.FETCH_CALLOG_FIRST_TIME);
                  startCallLogSync(true);
                  break;
              case FETCH_OLDER_CALLLOG_FROM_NATIVE_DB:
                  // button "more"
                  newState(State.FETCH_SMS_FROM_NATIVE_DB);
                  mActiveSyncHelper = new FetchSmsLogEvents(mContext, this, mDb, false);
                  setTimeout(READ_TIMELINES_TIMEOUT_MILLS);
                  break;
              case FETCH_CALLOG_FIRST_TIME:
                  // 1st sync
              case UPDATE_CALLOG_FROM_NATIVE:
                  // NAB changed.
                  newState(State.FETCH_SMS_FROM_NATIVE_DB);
                  mActiveSyncHelper = new FetchSmsLogEvents(mContext, this, mDb, true);
                  setTimeout(READ_TIMELINES_TIMEOUT_MILLS);
                  break;
              case FETCH_SMS_FROM_NATIVE_DB:
                  dequeueRequest(ServiceUiRequest.FETCH_TIMELINES.ordinal());
              case UPDATE_STATUSES:
                  // activities sync is over
                  mActiveSyncHelper = null;
                  newState(State.IDLE);
                  completeUiRequest(status, null);
                  break;
              default:
                  // Do nothing.
                  completeUiRequest(status, null);
                  break;
          }
      }
  
      
      /**
       * This method adds a request to get latest timelines.
       */
      protected void addGetNewPhonesCallsRequest() {
          LogUtils.logD("ActivitiesEngine addGetNewPhonesCallsRequest()");
          // I noticed that 1+ change events come when the native phone call log
          // is updated, the events also seem to be issued when changes on the NAB occur.
          // By passing "true" we try to filter out excessive calls and improve
          // the application performance (especially shortly after 1st time sync).    
          addUiRequestToQueue(ServiceUiRequest.UPDATE_PHONE_CALLS, null, true);
      }
  
      /**
       * This method adds a request to get latest timelines.
       */
      protected void addGetNewSMSRequest() {
          LogUtils.logD("ActivitiesEngine addGetNewSMSRequest()");
          addUiRequestToQueue(ServiceUiRequest.UPDATE_SMS, null, true);
      }
  
      /**
       * This method adds a request to get older timelines.
       */
      public void addOlderTimelinesRequest() {
          LogUtils.logD("ActivitiesEngine addOlderTimelinesRequest()");
          addUiRequestToQueue(ServiceUiRequest.FETCH_TIMELINES, null);
      }
  
      /**
       * This method adds a request to start sync of the most recent statuses from
       * Now+ server.
       */
      public void addStatusesSyncRequest() {
          LogUtils.logD("ActivityEngine addStatusesSyncRequest()");
          addUiRequestToQueue(ServiceUiRequest.UPDATE_STATUSES, null);
      }
  
      /**
       * This method adds a request to start sync of older statuses from Now+
       * server.
       */
      public void addGetOlderStatusesRequest() {
          LogUtils.logD("ActivityEngine addGetOlderStatusesRequest");
          addUiRequestToQueue(ServiceUiRequest.FETCH_STATUSES, null);
      }
  
      /**
       * Changes the state of the engine.
       * 
       * @param newState The new state
       */
      private void newState(State newState) {
          State oldState = mState;
          synchronized (mMutex) {
              mState = newState;
          }
          switch (mState) {
              case FETCH_OLDER_CALLLOG_FROM_NATIVE_DB:
              case UPDATE_STATUSES:
              case FETCH_STATUSES_FIRST_TIME:
                  fireNewState(ServiceUiRequest.UPDATING_UI, null);
                  break;
              case IDLE:
                  fireNewState(ServiceUiRequest.UPDATING_UI_FINISHED, null);
                  break;
              default:
                  // nothing to do
          }
  
          LogUtils.logV("ActivitiesEngine.newState(): " + oldState + " -> " + mState);
      }
  
      /**
       * This method fires states change of an engine to UI (between "busy" and
       * IDLE). This method is normally called after the new state has been
       * changed and published to ApplicationCache. The UI should refer to
       * ApplicationCache when processing this new ServiceRequest
       * 
       * @param request ServiceUIRequest UPDATING_UI or UPDATING_UI_FINISHED
       */
      public void fireNewState(ServiceUiRequest request, Bundle bundle) {
          UiAgent uiAgent = mEventCallback.getUiAgent();
          if (uiAgent != null && uiAgent.isSubscribed()) {
              uiAgent.sendUnsolicitedUiEvent(request, bundle);
          }
      }
  
      /**
       * This method stores the current busy state of engine in ApplicationCache
       * to make it available to UI. This method is normally called before setting
       * new state of the engine.
       * 
       * @param requestId int - the request Id for network communications, or the
       *            ServiceUIRequest ordinal for fetching/updating timelines
       * @param requestType one of UPDATING_STATUSES, FETCHING_OLDER_STATUSES,
       *            FETCHING_OLDER_TIMELINE
       */
      private void enqueueRequest(int requestId, ActivitiesState requestType) {
          synchronized (mQueueMutex) {
              if (!mActiveRequests.containsKey(requestId)) {
                  LogUtils.logI("ActivityEngine.enqueueRequest:" + requestId + ", " + requestType);
                  mActiveRequests.put(requestId, requestType);
                  cacheRequestType(requestType, true);
  
              } else {
                  LogUtils.logI("ActivityEngine.enqueueRequest: already have this type!" + requestId
                          + ", " + requestType);
              }
          }
      }
  
      /***
       * Add the given request type to the Application cache.
       *
       * @param requestType Request type ENUM.
       * @param value Boolean to set for given request type.
       */
      private final static void cacheRequestType(
              final ActivitiesState requestType, final boolean value) {
          switch (requestType) {
              case UPDATING_STATUSES:
                  ApplicationCache.setUpdatingStatuses(value);
                  break;
              case FETCHING_OLDER_STATUSES:
                  ApplicationCache.setFetchingOlderStatuses(value);
                  break;
              case FETCHING_OLDER_TIMELINE:
                  ApplicationCache.setFetchingOlderTimeline(value);
                  break;
          }
      }
  
      /**
       * This method stores removes a busy state of engine and males the change
       * available to UI through the ApplicationCache. This method is normally
       * called before setting new state of the engine.
       * 
       * @param requestId int - the request Id for network communications, or the
       *            ServiceUIRequest ordinal for fetching/updating timelines
       * @param requestType one of UPDATING_STATUSES, FETCHING_OLDER_STATUSES,
       *            FETCHING_OLDER_TIMELINE
       */
  
      private void dequeueRequest(int requestId) {
          synchronized (mQueueMutex) {
              ActivitiesState requestType = mActiveRequests.get(requestId);
              if (requestType != null) {
                  mActiveRequests.remove(requestId);
                  LogUtils.logI("ActivityEngine.dequeueRequest:" + requestId + ", " + requestType);
                  cacheRequestType(requestType, false);
              } else {
                  LogUtils.logI("ActivityEngine.dequeueRequest: the request is not in the queue!"
                          + requestId + ", " + requestType);
              }
          }
      }
  
      @Override
      public void onLoginStateChanged(boolean loggedIn) {
          mLastStatusUpdated = 0;
          mOldestStatusUpdated = 0;
      }
  
      /**
       * This method returns if the timelines have been updated with the previous
       * "show more" call or push message.
       * 
       * @return boolean TRUE if they timelines have been updated with the
       *         previous "show more" call or push message.
       */
      public boolean isTimelinesUpdated() {
          return mTimelinesUpdated;
      }
  
      /**
       * This method sets the state - if the timelines have been updated or not.
       * 
       * @param timelinesUpdated boolean
       */
      public void setTimelinesUpdated(boolean timelinesUpdated) {
          this.mTimelinesUpdated = timelinesUpdated;
      }
  
      @Override
      public void onReset() {
          
          // reset the engine as if it was just created
          super.onReset();
          mTimelinesUpdated = false;
          mLastStatusUpdated = 0;
          mOldestStatusUpdated = 0;
          mState = State.IDLE;
          mRequestActivitiesRequired = false;
          mActiveRequests.clear();
      }
  }
  