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