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.os.Bundle;
  import android.provider.CallLog.Calls;
  import android.telephony.PhoneNumberUtils;
  
  import com.vodafone360.people.database.DatabaseHelper;
  import com.vodafone360.people.database.tables.ActivitiesTable;
  import com.vodafone360.people.database.tables.ActivitiesTable.TimelineSummaryItem;
  import com.vodafone360.people.database.tables.StateTable;
  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.ServiceUtils;
  
  /**
   * Fetches call log events from the Native call log. These are treated as
   * Activities and displayed in Timeline UI.
   */
  public class FetchCallLogEvents implements ISyncHelper {
      /**
       * The maximum number of times the ISyncHelper should be called on this
       * sync (i.e. worker thread run loop).
       */
      private static final int MAX_NUMBER_OF_PAGES = 10;
      /**
       * The maximum number of Cursor rows that should be parsed on the current
       * run (i.e. while loop).
       */    
      private static final int MAX_CALL_LOG_ITEMS_PER_PAGE = 2;
  
      private static final int MAX_ITEMS_TO_WRITE = 10;
  
      // "-1" means number is unknown
      private static final String NATIVE_NUMBER_UNKNOWN_STRING = "-1";
      
      /**
       * Private phone number String used in Motorola Milestone
       * perhaps also used on other devices.
       */
      private static final String NATIVE_NUMBER_PRIVATE_STRING = "-2";
  
      private Context mContext;
  
      private ContentResolver mCr;
  
      private Cursor mNativeCursor;
  
      private ActivitiesEngine mEngine;
  
      private DatabaseHelper mDb;
  
      private final 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 phone call time
       */
      private long mOldestPhoneCall;
  
      /**
       * the current newest phone call time
       */
      private long mNewestPhoneCall;
  
      /**
       * Internal states for Call log event sync.
       */
      private static enum InternalState {
          IDLE, FETCHING_NEXT_PAGE
      }
  
      private InternalState mInternalState;
  
      /**
       * Set of Call log items to be fetched by query on Native call-log.
       */
      private static final String[] CALL_LOG_PROJECTION = new String[] {
              Calls._ID, Calls.NUMBER, Calls.DATE, Calls.TYPE,
      };
  
      private static final int COLUMN_CALLLOG_ID = 0;
  
      private static final int COLUMN_CALLLOG_PHONE = 1;
  
      private static final int COLUMN_CALLLOG_DATE = 2;
  
      private static final int COLUMN_CALLLOG_TYPE = 3;
  
      /**
       * Constructor.
       * 
       * @param context Context - actually RemoteServe's Context.
       * @param engine Handle to ActivitiesEngine.
       * @param db Handle to DatabaseHelper.
       * @param refresh boolean - true if new phone call need to be fetched, false
       *            - if older
       */
      FetchCallLogEvents(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 call-log items or complete call-log sync operation.
       */
      @Override
      public void run() {
          switch (mInternalState) {
              case IDLE:
                  startSyncCallLog();
                  break;
              case FETCHING_NEXT_PAGE:
                  syncNextPage();
                  break;
              default:
                  mStatus = ServiceStatus.ERROR_NOT_READY;
                  complete(mStatus);
                  break;
          }
      }
  
      /**
       * Cancel fetch of call log events. Close Cursor to Native Call log. Reset
       * state to IDLE.
       */
      @Override
      public void cancel() {
          if (mNativeCursor != null) {
              mNativeCursor.close();
          }
          mInternalState = InternalState.IDLE;
      }
  
      /**
       * Start sync of call events from Native call log. Use Timeline last update
       * time-stamp to ensure we only fetch 'new' events.
       */
      private void startSyncCallLog() {
          mOldestPhoneCall = StateTable.fetchOldestPhoneCallTime(mDb.getReadableDatabase());
          mNewestPhoneCall = StateTable.fetchLatestPhoneCallTime(mDb.getReadableDatabase());
          // at 1st sync the StateTable contains no value: so for "refresh" set
          // value to 0, for "more" to current time
          if (mOldestPhoneCall == 0) {
              mOldestPhoneCall = System.currentTimeMillis();
              StateTable.modifyOldestPhoneCallTime(mOldestPhoneCall, mDb.getWritableDatabase());
          }
          String whereClause = mRefresh ? Calls.DATE + ">" + mNewestPhoneCall : Calls.DATE + "<"
                  + mOldestPhoneCall;
          mNativeCursor = mCr.query(Calls.CONTENT_URI, CALL_LOG_PROJECTION, whereClause, null,
                  Calls.DATE + " DESC");
  
          mInternalState = InternalState.FETCHING_NEXT_PAGE;
          syncNextPage();
      }
  
      /**
       * Sync next page of call-log events (page-size is 2).
       */
      private void syncNextPage() {
          if (mNativeCursor.isAfterLast()) {
              complete(mStatus);
              return;
          }
  
          boolean finished = false;
          if (mPageCount < MAX_NUMBER_OF_PAGES) {
              int id = 0;
              int count = 0;
              long timestamp = 0;
              while (count < MAX_CALL_LOG_ITEMS_PER_PAGE && mNativeCursor.moveToNext()) {
                  id = mNativeCursor.getInt(COLUMN_CALLLOG_ID);
                  timestamp = mNativeCursor.getLong(COLUMN_CALLLOG_DATE);
                  if (mRefresh) {
                      if (timestamp < mNewestPhoneCall) {
                          finished = true;
                          break;
                      }
                  } else {
                      if (timestamp > mOldestPhoneCall) {
                          finished = true;
                          break;
                      }
                  }
                  addCallLogData(id);
                  count++;
              }
              mPageCount++;
              if ((count == MAX_CALL_LOG_ITEMS_PER_PAGE && mSyncItemList.size() == MAX_ITEMS_TO_WRITE)
                      || (count < MAX_CALL_LOG_ITEMS_PER_PAGE) || (mPageCount == MAX_NUMBER_OF_PAGES)) {
                  ServiceStatus status = updateDatabase();
                  if (ServiceStatus.SUCCESS != status) {
                      mStatus = status; 
                      complete(mStatus);
                      return;
                  }
                  mEngine.fireNewState(ServiceUiRequest.DATABASE_CHANGED_EVENT, new Bundle());
              }
          } else {
              finished = true;
          }
          if (finished) {
              saveTimestamp();
              complete(mStatus);
          }
      }
  
      private ServiceStatus updateDatabase() {
          ServiceStatus status = mDb.addTimelineEvents(mSyncItemList, true);
          updateTimeStamps();
          saveTimestamp();
          mSyncItemList.clear();
  
          return status;
      }
  
  
      /**
       * This method goes through the event list and updates the current newest
       * and oldest phone call 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 (mNewestPhoneCall < max) {
                  mNewestPhoneCall = max;
              }
              if (mOldestPhoneCall > min) {
                  mOldestPhoneCall = min;
              }
          }
      }
  
      /**
       * This method updates the newest and oldest phone calls timestamps in the
       * database.
       */
      private void saveTimestamp() {
          long saved = StateTable.fetchOldestPhoneCallTime(mDb.getReadableDatabase());
          if (mOldestPhoneCall < saved) {
              StateTable.modifyOldestPhoneCallTime(mOldestPhoneCall, mDb.getWritableDatabase());
              LogUtils.logD("FetchCallLogEvents saveTimestamp: oldest timeline update set to = "
                      + mOldestPhoneCall);
              mStatus = ServiceStatus.UPDATED_TIMELINES_FROM_NATIVE;
          }
          saved = StateTable.fetchLatestPhoneCallTime(mDb.getReadableDatabase());
          if (mNewestPhoneCall > saved) {
              StateTable.modifyLatestPhoneCallTime(mNewestPhoneCall, mDb.getWritableDatabase());
              LogUtils.logD("FetchCallLogEvents saveTimestamp: newest timeline update set to = "
                      + mNewestPhoneCall);
              mStatus = ServiceStatus.UPDATED_TIMELINES_FROM_NATIVE;
          }
      }
  
      /**
       * Create TimelineSummaryItem from Native call-log item.
       * 
       * @param id ID of item from Native log.
       */
      private void addCallLogData(int id) {
          TimelineSummaryItem item = new TimelineSummaryItem();
  
          final String phoneNo = mNativeCursor.getString(COLUMN_CALLLOG_PHONE);
  
          item.mNativeItemId = id;
          item.mNativeItemType = ActivitiesTable.TimelineNativeTypes.CallLog.ordinal();
          item.mType = nativeTypeToNpType(mNativeCursor.getInt(COLUMN_CALLLOG_TYPE));
          item.mTimestamp = mNativeCursor.getLong(COLUMN_CALLLOG_DATE);
          item.mTitle = DateFormat.getDateInstance().format(new Date(item.mTimestamp));
          item.mDescription = null;
  
          if (phoneNo.compareToIgnoreCase(NATIVE_NUMBER_UNKNOWN_STRING) != 0
                  && phoneNo.compareToIgnoreCase(NATIVE_NUMBER_PRIVATE_STRING) != 0) {
              item.mContactName = PhoneNumberUtils.formatNumber(phoneNo);
              item.mContactAddress = phoneNo;
          } else {
              item.mContactName = null;
              item.mContactAddress = null;
          }
  
          Contact c = new Contact();
          ContactDetail phoneDetail = new ContactDetail();
          ServiceStatus status = mDb.fetchContactInfo(phoneNo, 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 = name.toString();
                          }
                          break;
                      case VCARD_IMADDRESS:
                          item.mContactNetwork = d.alt;
                          break;
                      default:
                          // do nothing
                          break;
                  }
              }
              item.mDescription = ServiceUtils.getDetailTypeString(mContext.getResources(),
                      phoneDetail.keyType)
                      + " " + phoneNo;
          }
          if (item.mContactId == null) {
              LogUtils.logI("FetchCallLogEvents.addCallLogData: id " + item.mNativeItemId + ", time "
                      + item.mTitle + ", number " + item.mContactName);
          } else {
              LogUtils.logI("FetchCallLogEvents.addCallLogData: id " + item.mNativeItemId
                      + ", name = " + item.mContactName + ", time " + item.mTitle + ", number "
                      + item.mDescription);
          }
          mSyncItemList.add(item);
      }
  
      /**
       * Completion of fetch from Native call log. Notify ActivitiesEngine that
       * call-log sync. has completed.
       * 
       * @param status ServiceStatus containing result of sync.
       */
      private void complete(ServiceStatus status) {
          mEngine.onSyncHelperComplete(status);
          cancel();
      }
  
      /**
       * Convert native call type to type stored in People's ActivityItem.
       * 
       * @param nativeType Native call type.
       * @return People's ActivityItem call type.
       */
      private static ActivityItem.Type nativeTypeToNpType(int nativeType) {
          switch (nativeType) {
              case Calls.INCOMING_TYPE:
                  return ActivityItem.Type.CALL_RECEIVED;
              case Calls.MISSED_TYPE:
                  return ActivityItem.Type.CALL_MISSED;
              case Calls.OUTGOING_TYPE:
                  return ActivityItem.Type.CALL_DIALED;
              default:
                  LogUtils.logW("FetchCallLogEvents.nativeTypeToNpType() "
                          + "Unknown type[" + nativeType + "]");
                  return null;
          }
      }
  }