/*
* 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.meprofile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import android.content.Context;
import com.vodafone360.people.database.DatabaseHelper;
import com.vodafone360.people.database.tables.ContactSummaryTable;
import com.vodafone360.people.database.tables.StateTable;
import com.vodafone360.people.datatypes.BaseDataType;
import com.vodafone360.people.datatypes.Contact;
import com.vodafone360.people.datatypes.ContactChanges;
import com.vodafone360.people.datatypes.ContactDetail;
import com.vodafone360.people.datatypes.ExternalResponseObject;
import com.vodafone360.people.datatypes.PushEvent;
import com.vodafone360.people.datatypes.SystemNotification;
import com.vodafone360.people.datatypes.UserProfile;
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.service.PersistSettings;
import com.vodafone360.people.service.ServiceStatus;
import com.vodafone360.people.service.ServiceUiRequest;
import com.vodafone360.people.service.agent.NetworkAgent;
import com.vodafone360.people.service.io.QueueManager;
import com.vodafone360.people.service.io.Request;
import com.vodafone360.people.service.io.ResponseQueue.DecodedResponse;
import com.vodafone360.people.service.io.api.Contacts;
import com.vodafone360.people.utils.LogUtils;
import com.vodafone360.people.utils.ThumbnailUtils;
import com.vodafone360.people.utils.WidgetUtils;
/**
* This is an implementation for an engine to synchronize
* Me profile data.
*
*/
public class SyncMeEngine extends BaseEngine {
/**
* Current engine state.
*/
private State mState = State.IDLE;
/**
* Database.
*/
private DatabaseHelper mDbHelper;
/**
* The latest revision of Me Profile.
*/
private long mFromRevision;
/**
* The Me Profile, as it was uploaded.
*/
private ArrayList<ContactDetail> mUploadedMeDetails;
/**
* Indicates if the first time sync has been ever initiated.
*/
private boolean mFirstTimeSyncStarted;
/**
* Indicates if the first time sync has been completed.
*/
private boolean mFirstTimeMeSyncComplete;
/**
* Defines the contact sync mode. The mode determines the sequence in which
* the contact sync processors are run.
*/
private enum State {
/**
* The state when the engine is not running and has nothing on the todo list.
*/
IDLE,
/**
* The state when the engine is downloading Me Profile from server.
*/
FETCHING_ME_PROFILE_CHANGES,
/**
* The state when the engine is uploading Me Profile to server.
*/
UPDATING_ME_PROFILE,
/**
* The state when the engine is uploading Me Profile status message.
*/
UPDATING_ME_PRESENCE_TEXT,
/**
* The state when the engine is downloading Me Profile thumbnail.
*/
FETCHING_ME_PROFILE_THUMBNAIL,
}
/**
* The service context.
*/
private Context mContext;
/**
* The constructor.
* @param eventCallback IEngineEventCallback
* @param db DatabaseHelper - database.
*/
public SyncMeEngine(final Context context, final IEngineEventCallback eventCallback, DatabaseHelper db) {
super(eventCallback);
mEngineId = EngineId.SYNCME_ENGINE;
mDbHelper = db;
mContext = context;
mFromRevision = StateTable.fetchMeProfileRevision(mDbHelper.getReadableDatabase());
}
@Override
public long getNextRunTime() {
if (!isReady()) {
return -1;
}
if (mFirstTimeSyncStarted && !mFirstTimeMeSyncComplete && (mState == State.IDLE)) {
return 0;
}
if (isCommsResponseOutstanding()) {
return 0;
}
if (isUiRequestOutstanding()) {
return 0;
}
return getCurrentTimeout();
}
/**
* The condition for the sync me engine run.
* @return boolean - TRUE when the engine is ready to run.
*/
private boolean isReady() {
return EngineManager.getInstance().getLoginEngine().isLoggedIn() && checkConnectivity()
&& mFirstTimeSyncStarted;
}
@Override
public void run() {
LogUtils.logD("SyncMeEngine run");
processTimeout();
if (isCommsResponseOutstanding() && processCommsInQueue()) {
return;
}
if (isUiRequestOutstanding()) {
processUiQueue();
}
if (mFromRevision == 0 && (mState == State.IDLE)) {
addGetMeProfileContactRequest();
}
}
@Override
public void onCreate() {
PersistSettings setting = mDbHelper.fetchOption(
PersistSettings.Option.FIRST_TIME_MESYNC_STARTED);
if (setting != null) {
mFirstTimeSyncStarted = setting.getFirstTimeMeSyncStarted();
}
setting = mDbHelper.fetchOption(
PersistSettings.Option.FIRST_TIME_MESYNC_COMPLETE);
if (setting != null) {
mFirstTimeMeSyncComplete = setting.getFirstTimeMeSyncComplete();
}
}
@Override
public void onDestroy() {
}
@Override
protected void onRequestComplete() {
}
@Override
protected void onTimeoutEvent() {
}
@Override
protected final void processUiRequest(final ServiceUiRequest requestId, Object data) {
switch (requestId) {
case UPDATE_ME_PROFILE:
uploadMeProfile();
break;
case GET_ME_PROFILE:
getMeProfileChanges();
break;
case UPLOAD_ME_STATUS:
uploadStatusUpdate(SyncMeDbUtils.updateStatus(mDbHelper, (String)data));
break;
default:
// do nothing.
break;
}
}
/**
* Sends a GetMyChanges request to the server, with the current version of
* the me profile used as a parameter.
*/
private void getMeProfileChanges() {
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
return;
}
newState(State.FETCHING_ME_PROFILE_CHANGES);
setReqId(Contacts.getMyChanges(this, mFromRevision));
}
/**
* The call to download the thumbnail picture for the me profile.
* @param url String - picture url of Me Profile (comes with getMyChanges())
* @param localContactId long - local contact id of Me Profile
*/
private void downloadMeProfileThumbnail(final String url, final long localContactId) {
if (NetworkAgent.getAgentState() == NetworkAgent.AgentState.CONNECTED) {
Request request = new Request(url, ThumbnailUtils.REQUEST_THUMBNAIL_URI, engineId());
newState(State.FETCHING_ME_PROFILE_THUMBNAIL);
setReqId(QueueManager.getInstance().addRequestAndNotify(request));
}
}
/**
* Starts uploading a status update to the server and ignores all other
* @param statusDetail - status ContactDetail
*/
private void uploadStatusUpdate(final ContactDetail statusDetail) {
LogUtils.logE("SyncMeProfile uploadStatusUpdate()");
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
LogUtils.logE("SyncMeProfile uploadStatusUpdate: no internet connection");
return;
}
if (statusDetail == null) {
LogUtils.logE("SyncMeProfile uploadStatusUpdate: null status can't be posted");
return;
}
newState(State.UPDATING_ME_PRESENCE_TEXT);
List<ContactDetail> details = new ArrayList<ContactDetail>();
statusDetail.updated = null;
details.add(statusDetail);
setReqId(Contacts.setMe(this, details, null, null));
}
/**
* * Sends a SetMe request to the server in the case that the me profile has
* been changed locally. If the me profile thumbnail has always been changed
* it will also be uploaded to the server.
*/
private void uploadMeProfile() {
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
return;
}
newState(State.UPDATING_ME_PROFILE);
Contact meProfile = new Contact();
mDbHelper.fetchContact(SyncMeDbUtils.getMeProfileLocalContactId(mDbHelper), meProfile);
mUploadedMeDetails = SyncMeDbUtils.saveContactDetailChanges(mDbHelper, meProfile);
setReqId(Contacts.setMe(this, mUploadedMeDetails, meProfile.aboutMe, meProfile.gender));
}
/**
* 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;
}
/**
* Changes the state of the engine.
* @param newState The new state
*/
private void newState(final State newState) {
State oldState = mState;
mState = newState;
LogUtils.logV("SyncMeEngine newState(): " + oldState + " -> " + mState);
}
/**
* Called by framework when a response to a server request is received.
* @param resp The response received
*/
public final void processCommsResponse(final DecodedResponse resp) {
if (!processPushEvent(resp)) {
switch (mState) {
case FETCHING_ME_PROFILE_CHANGES:
processGetMyChangesResponse(resp);
break;
case UPDATING_ME_PRESENCE_TEXT:
processUpdateStatusResponse(resp);
break;
case UPDATING_ME_PROFILE:
processSetMeResponse(resp);
break;
case FETCHING_ME_PROFILE_THUMBNAIL:
processMeProfileThumbnailResponse(resp);
break;
default:
// do nothing.
break;
}
}
}
/**
* This method stores the thumbnail picture for the me profile
* @param resp Response - normally contains ExternalResponseObject for the
* picture
*/
private void processMeProfileThumbnailResponse(final DecodedResponse resp) {
if (resp.mDataTypes.size()==0){
LogUtils.logE("SyncMeProfile processMeProfileThumbnailResponse():"
+ SystemNotification.SysNotificationCode.EXTERNAL_HTTP_ERROR);
completeUiRequest(ServiceStatus.ERROR_COMMS_BAD_RESPONSE);
return;
}
Contact currentMeProfile = new Contact();
ServiceStatus status = SyncMeDbUtils.fetchMeProfile(mDbHelper, currentMeProfile);
if (status == ServiceStatus.SUCCESS) {
if (resp.mReqId == null || resp.mReqId == 0) {
if (resp.mDataTypes.get(0).getType() == BaseDataType.SYSTEM_NOTIFICATION_DATA_TYPE
&& ((SystemNotification)resp.mDataTypes.get(0)).getSysCode() == SystemNotification.SysNotificationCode.EXTERNAL_HTTP_ERROR) {
LogUtils.logE("SyncMeProfile processMeProfileThumbnailResponse():"
+ SystemNotification.SysNotificationCode.EXTERNAL_HTTP_ERROR);
}
completeUiRequest(status);
return;
} else if (resp.mDataTypes.get(0).getType() == BaseDataType.SYSTEM_NOTIFICATION_DATA_TYPE) {
if (((SystemNotification)resp.mDataTypes.get(0)).getSysCode() == SystemNotification.SysNotificationCode.EXTERNAL_HTTP_ERROR) {
LogUtils.logE("SyncMeProfile processMeProfileThumbnailResponse():"
+ SystemNotification.SysNotificationCode.EXTERNAL_HTTP_ERROR);
}
completeUiRequest(status);
return;
}
status = BaseEngine
.getResponseStatus(BaseDataType.EXTERNAL_RESPONSE_OBJECT_DATA_TYPE, resp.mDataTypes);
if (status != ServiceStatus.SUCCESS) {
completeUiRequest(ServiceStatus.ERROR_COMMS_BAD_RESPONSE);
LogUtils
.logE("SyncMeProfile processMeProfileThumbnailResponse() - Can't read response");
return;
}
if (resp.mDataTypes == null || resp.mDataTypes.isEmpty()) {
LogUtils
.logE("SyncMeProfile processMeProfileThumbnailResponse() - Datatypes are null");
completeUiRequest(ServiceStatus.ERROR_COMMS_BAD_RESPONSE);
return;
}
// finally save the thumbnails
ExternalResponseObject ext = (ExternalResponseObject)resp.mDataTypes.get(0);
if (ext.mBody == null) {
LogUtils.logE("SyncMeProfile processMeProfileThumbnailResponse() - no body");
completeUiRequest(ServiceStatus.ERROR_COMMS_BAD_RESPONSE);
return;
}
try {
ThumbnailUtils.saveExternalResponseObjectToFile(currentMeProfile.localContactID,
ext);
ContactSummaryTable.modifyPictureLoadedFlag(currentMeProfile.localContactID, true,
mDbHelper.getWritableDatabase());
mDbHelper.markMeProfileAvatarChanged();
} catch (IOException e) {
LogUtils.logE("SyncMeProfile processMeProfileThumbnailResponse()", e);
completeUiRequest(ServiceStatus.ERROR_COMMS_BAD_RESPONSE);
}
}
completeUiRequest(status);
}
/**
* Processes the response from a GetMyChanges request. The me profile data
* will be merged in the local database if the response is successful.
* Otherwise the processor will complete with a suitable error.
* @param resp Response from server.
*/
private void processGetMyChangesResponse(final DecodedResponse resp) {
LogUtils.logD("SyncMeEngine processGetMyChangesResponse()");
ServiceStatus status = BaseEngine.getResponseStatus(BaseDataType.CONTACT_CHANGES_DATA_TYPE,
resp.mDataTypes);
if (status == ServiceStatus.SUCCESS) {
ContactChanges changes = (ContactChanges)resp.mDataTypes.get(0);
Contact currentMeProfile = new Contact();
status = SyncMeDbUtils.fetchMeProfile(mDbHelper, currentMeProfile);
switch (status) {
case SUCCESS:
SyncMeDbUtils.updateMeProfile(mDbHelper, currentMeProfile, changes.mUserProfile);
break;
case ERROR_NOT_FOUND: // this is the 1st time sync
currentMeProfile.copy(changes.mUserProfile);
status = SyncMeDbUtils.setMeProfile(mDbHelper, currentMeProfile);
setFirstTimeMeSyncComplete(true);
break;
default:
completeUiRequest(status);
return;
}
final String url = fetchThumbnailUrlFromProfile(changes.mUserProfile);
if (url != null) {
downloadMeProfileThumbnail(url, currentMeProfile.localContactID);
} else {
completeUiRequest(status);
}
storeMeProfileRevisionInDb(changes.mCurrentServerVersion);
} else {
completeUiRequest(status);
}
}
/**
*
* Stores the new server revision of the me profile passed to this method in the database.
*
* @param newServerRevision The new revision of the me profile.
*
*/
private void storeMeProfileRevisionInDb(final int newServerRevision) {
mFromRevision = newServerRevision;
StateTable.modifyMeProfileRevision(mFromRevision, mDbHelper.getWritableDatabase());
LogUtils.logI("SyncMeEngine.processGetMyChangesResponse() " +
"Stored fromRevision: " + mFromRevision);
}
/**
*
* Fetches the thumbnail URL if there is one in the passed profile.
*
* @param profile The profile which contains the URL of the thumbnail in its details.
*
* @return Returns the thumbnail if it was found as part of the PHOTO-detail or null if the
* passed profile was null, the PHOTO-detail was not found or all of the details in the profile
* are null.
*
*/
private final String fetchThumbnailUrlFromProfile(final UserProfile profile) {
if (null == profile) {
return null;
}
List<ContactDetail> details = profile.details;
if (null == details) {
return null;
}
Iterator<ContactDetail> iterator = details.iterator();
while (iterator.hasNext()) {
ContactDetail detail = iterator.next();
if (null == detail) {
continue;
}
if (ContactDetail.DetailKeys.PHOTO.equals(detail.key)) {
return detail.value;
}
}
return null;
}
/**
* Processes the response from a SetMe request. If successful, the server
* IDs will be stored in the local database if they have changed. Otherwise
* the processor will complete with a suitable error.
* @param resp Response from server.
*/
private void processSetMeResponse(final DecodedResponse resp) {
LogUtils.logD("SyncMeProfile.processMeProfileUpdateResponse()");
ServiceStatus status = BaseEngine.getResponseStatus(BaseDataType.CONTACT_CHANGES_DATA_TYPE,
resp.mDataTypes);
if (status == ServiceStatus.SUCCESS) {
ContactChanges result = (ContactChanges) resp.mDataTypes.get(0);
SyncMeDbUtils.updateMeProfileDbDetailIds(mDbHelper, mUploadedMeDetails, result);
if (updateRevisionPostUpdate(result.mServerRevisionBefore, result.mServerRevisionAfter,
mFromRevision, mDbHelper)) {
mFromRevision = result.mServerRevisionAfter;
}
}
completeUiRequest(status);
}
/**
* This method processes the response to status update by setMe() method
* @param resp Response - the expected response datatype is ContactChanges
*/
private void processUpdateStatusResponse(final DecodedResponse resp) {
LogUtils.logD("SyncMeDbUtils processUpdateStatusResponse()");
ServiceStatus status = BaseEngine.getResponseStatus(BaseDataType.CONTACT_CHANGES_DATA_TYPE,
resp.mDataTypes);
if (status == ServiceStatus.SUCCESS) {
ContactChanges result = (ContactChanges) resp.mDataTypes.get(0);
LogUtils.logI("SyncMeProfile.processUpdateStatusResponse() - Me profile userId = "
+ result.mUserProfile.userID);
SyncMeDbUtils.savePresenceStatusResponse(mDbHelper, result);
}
completeUiRequest(status);
}
@Override
protected void completeUiRequest(ServiceStatus status) {
super.completeUiRequest(status);
newState(State.IDLE);
WidgetUtils.kickWidgetUpdateNow(mContext);
}
/**
* Updates the revision of the me profile in the local state table after the
* SetMe has completed. This will only happen if the version of the me
* profile on the server before the update matches our previous version.
* @param before Version before the update
* @param after Version after the update
* @param currentFromRevision Current version from our database
* @param db Database helper used for storing the change
* @return true if the update was done, false otherwise.
*/
private static boolean updateRevisionPostUpdate(final Integer before, final Integer after,
final long currentFromRevision, final DatabaseHelper db) {
if (before == null || after == null) {
return false;
}
if (!before.equals(currentFromRevision)) {
LogUtils
.logW("SyncMeProfile.updateRevisionPostUpdate - Previous version is not as expected, current version="
+ currentFromRevision + ", server before=" + before + ", server after=" + after);
return false;
} else {
StateTable.modifyMeProfileRevision(after, db.getWritableDatabase());
return true;
}
}
/**
* This method adds an external request to Contacts/setMe() method to update
* the Me Profile...
* @param meProfile Contact - contact to be pushed to the server
*/
public void addUpdateMeProfileContactRequest() {
LogUtils.logV("SyncMeEngine addUpdateMeProfileContactRequest()");
addUiRequestToQueue(ServiceUiRequest.UPDATE_ME_PROFILE, null);
}
/**
* This method adds an external request to Contacts/setMe() method to update
* the Me Profile status...
* @param textStatus String - the new me profile status to be pushed to the
* server
*/
public void addUpdateMyStatusRequest(String textStatus) {
LogUtils.logV("SyncMeEngine addUpdateMyStatusRequest()");
addUiRequestToQueue(ServiceUiRequest.UPLOAD_ME_STATUS, textStatus);
}
/**
* This method adds an external request to Contacts/getMyChanges() method to
* update the Me Profile status server. Is called when "pc "push message is
* received
*/
private void addGetMeProfileContactRequest() {
LogUtils.logV("SyncMeEngine addGetMeProfileContactRequest()");
addUiRequestToQueue(ServiceUiRequest.GET_ME_PROFILE, null);
}
/**
* This method adds an external request to Contacts/getMyChanges() method to
* update the Me Profile status server, is called by the UI at the 1st sync.
*/
public void addGetMeProfileContactFirstTimeRequest() {
LogUtils.logV("SyncMeEngine addGetMeProfileContactFirstTimeRequest()");
setFirstTimeSyncStarted(true);
addUiRequestToQueue(ServiceUiRequest.GET_ME_PROFILE, null);
}
/**
* This method process the "pc" push event.
* @param resp Response - server response normally containing a "pc"
* PushEvent data type
* @return boolean - TRUE if a push event was found in the response
*/
private boolean processPushEvent(final DecodedResponse resp) {
if (resp.mDataTypes == null || resp.mDataTypes.size() == 0) {
return false;
}
BaseDataType dataType = resp.mDataTypes.get(0);
if ((dataType == null) || dataType.getType() != BaseDataType.PUSH_EVENT_DATA_TYPE) {
return false;
}
PushEvent pushEvent = (PushEvent) dataType;
LogUtils.logV("SyncMeEngine processPushMessage():" + pushEvent.mMessageType);
switch (pushEvent.mMessageType) {
case PROFILE_CHANGE:
addGetMeProfileContactRequest();
break;
default:
break;
}
return true;
}
/**
* Helper function to update the database when the state of the
* {@link #mFirstTimeMeSyncStarted} flag changes.
* @param value New value to the flag. True indicates that first time sync
* has been started. The flag is never set to false again by the
* engine, it will be only set to false when a remove user data
* is done (and the database is deleted).
* @return SUCCESS or a suitable error code if the database could not be
* updated.
*/
private ServiceStatus setFirstTimeSyncStarted(final boolean value) {
if (mFirstTimeSyncStarted == value) {
return ServiceStatus.SUCCESS;
}
PersistSettings setting = new PersistSettings();
setting.putFirstTimeMeSyncStarted(value);
ServiceStatus status = mDbHelper.setOption(setting);
if (ServiceStatus.SUCCESS == status) {
synchronized (this) {
mFirstTimeSyncStarted = value;
}
}
return status;
}
/**
* Helper function to update the database when the state of the
* {@link #mFirstTimeMeSyncComplete} flag changes.
* @param value New value to the flag. True indicates that first time sync
* has been completed. The flag is never set to false again by
* the engine, it will be only set to false when a remove user
* data is done (and the database is deleted).
* @return SUCCESS or a suitable error code if the database could not be
* updated.
*/
private ServiceStatus setFirstTimeMeSyncComplete(final boolean value) {
if (mFirstTimeMeSyncComplete == value) {
return ServiceStatus.SUCCESS;
}
PersistSettings setting = new PersistSettings();
setting.putFirstTimeMeSyncComplete(value);
ServiceStatus status = mDbHelper.setOption(setting);
if (ServiceStatus.SUCCESS == status) {
synchronized (this) {
mFirstTimeMeSyncComplete = value;
}
}
return status;
}
/**
* This method needs to be called as part of removeAllData()/changeUser()
* routine.
*/
/** {@inheritDoc} */
@Override
public final void onReset() {
// reset the engine as if it was just created
super.onReset();
mFirstTimeMeSyncComplete = false;
mFirstTimeSyncStarted = false;
mFromRevision = 0;
mUploadedMeDetails = null;
mState = State.IDLE;
}
/**
* This method TRUE if the Me Profile has been synced once.
* @return boolean - TRUE if the Me Profile has been synced once.
*/
public final boolean isFirstTimeMeSyncComplete() {
return mFirstTimeMeSyncComplete;
}
}