/* * Funambol is a mobile platform developed by Funambol, Inc. * Copyright (C) 2008 Funambol, Inc. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by * the Free Software Foundation with the addition of the following permission * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA. * * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License * version 3, these Appropriate Legal Notices must retain the display of the * "Powered by Funambol" logo. If the display of the logo is not reasonably * feasible for technical reasons, the Appropriate Legal Notices must display * the words "Powered by Funambol". */ package com.funambol.client.engine; import java.util.Vector; import java.util.Hashtable; import java.util.Enumeration; import com.funambol.client.source.AppSyncSource; import com.funambol.client.source.AppSyncSourceManager; import com.funambol.client.configuration.Configuration; import com.funambol.client.customization.Customization; import com.funambol.client.push.SyncSchedulerListener; import com.funambol.syncml.protocol.SyncML; import com.funambol.syncml.spds.CompressedSyncException; import com.funambol.syncml.spds.DeviceConfig; import com.funambol.syncml.spds.SyncManager; import com.funambol.sync.SyncSource; import com.funambol.sync.SyncException; import com.funambol.sync.SyncConfig; import com.funambol.sync.SyncManagerI; import com.funambol.syncml.protocol.DevInf; import com.funambol.sapisync.SapiSyncManager; import com.funambol.platform.NetworkStatus; import com.funambol.util.TransportAgent; import com.funambol.util.StringUtil; import com.funambol.util.Log; /** * This class represents an engine for synchronizations. It wraps the APIs and * in particular it is built on top of the SyncScheduler. This class has the * following main goals: * * 1) Perform some basic checks before firing a sync. For example it checks if * radio signal is good. These checks are platform dependent and not * performed by the APIs * 2) Incapsulate error handling and sync threading. When a sync is requested it * is run in a separate thread, and this thread is monitored by the * SyncEngine which intercepts exceptions and handle them * 3) Support compression error recovering. If a sync throws an error because * compression is not supported, then the sync is resumed without compression * 4) Add a listener for the entire sync. Each sync source has its own listener * for source specific events, but the SyncEngine has a different listener * that generates events global to the entire synchronization. * */ public class SyncEngine implements SyncSchedulerListener { private static final String TAG_LOG = "SyncEngine"; private SyncEngineListener listener = null; protected Customization customization = null; protected AppSyncSourceManager appSyncSourceManager = null; protected Configuration configuration = null; private boolean isSynchronizing = false; private AppSyncSource currentSource = null; private Vector appSourcesRequest = new Vector(); private SyncThread syncThread; private NetworkStatus networkStatus; private boolean spawnThread = true; private TransportAgent customTransportAgent = null; private Hashtable customHeaders = null; public SyncEngine(Customization customization, Configuration configuration, AppSyncSourceManager appSyncSourceManager, NetworkStatus networkStatus) { this.customization = customization; this.configuration = configuration; this.appSyncSourceManager = appSyncSourceManager; this.networkStatus = networkStatus; } public void setListener(SyncEngineListener listener) { this.listener = listener; } /** * Gets the current listener. */ public SyncEngineListener getListener() { return listener; } public void setSpawnThread(boolean value) { spawnThread = value; } public void setNetworkStatus(NetworkStatus networkStatus) { this.networkStatus = networkStatus; } public void cancelSync() { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Cancelling sync"); } if (isSynchronizing && syncThread != null) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Cancelling sync on sync thread"); } syncThread.cancelSync(); } } public void setTransportAgent(TransportAgent ta) { this.customTransportAgent = ta; } public void addTranportAgentHeaders(Hashtable headers) { this.customHeaders = headers; } /** * SyncSchedulerListener callback. This method is invoked when a sync * programmed in the SyncScheduler shall be fired. */ public void sync(Object[] requestContent){ appSourcesRequest.removeAllElements(); for (int i =0; i<requestContent.length; i++){ appSourcesRequest.addElement(requestContent[i]); } synchronize(appSourcesRequest); } /** * Returns true iff a sync is in progress */ public boolean isSynchronizing(){ return isSynchronizing; } /** * Returns the source which is currently being synchronized. This method * returns a non null value only if isSynchronizing returns true. The method * may return null even during a synchronization. In particular a * synchronization is requested and this immediately triggers the * "isSynchronizing" but the currentSource is not set until the source * really starts synchronizing. Users must be ready to handle null return * values. */ public AppSyncSource getCurrentSource() { return currentSource; } public boolean synchronize(Vector sources) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "synchronize"); } isSynchronizing = true; if (listener != null) { listener.beginSync(); } if ( configuration.getUsername() == null || configuration.getUsername().length() == 0 || configuration.getPassword() == null || configuration.getPassword().length() == 0) { if (listener != null) { listener.noCredentials(); } syncEnded(); return false; } // Safety check. If there are no ready to use sources the sync is // stopped. This case should be captured earlier in the flow, but // just in case.... if (appSyncSourceManager.numberOfEnabledAndWorkingSources() == 0) { if (listener != null) { listener.noSources(); } syncEnded(); return false; } if (networkStatus != null && !networkStatus.isConnected()) { if (networkStatus.isRadioOff()) { if (listener != null) { listener.noConnection(); } } else { if (listener != null) { listener.noSignal(); } } syncEnded(); return false; } Vector checkSources = new Vector(); for (int x = 0; x < sources.size(); x++) { AppSyncSource appSource = (AppSyncSource) sources.elementAt(x); SyncSource source = appSource.getSyncSource(); int mode = source.getConfig().getSyncMode(); if (mode != SyncSource.FULL_UPLOAD && mode != SyncSource.FULL_DOWNLOAD) { checkSources.addElement(source); } } if (sources.size() == 0) { if (listener != null) { listener.noSources(); } syncEnded(); return false; } else { if (listener != null && listener.isCancelled()) { syncEnded(); return false; } syncThread = new SyncThread(sources); if (spawnThread) { syncThread.setPriority(Thread.MIN_PRIORITY); syncThread.start(); } else { syncThread.sync(); } }// end if return true; } /** * Utility method to be invoked at the end of a sync. The method resets all * the necessary internal variables and invoke the listener. */ private void syncEnded() { isSynchronizing = false; currentSource = null; // Save the latest authentication config. if (configuration != null) { if (configuration.getSyncConfig() != null) { configuration.setClientNonce(configuration.getSyncConfig().clientNonce); } configuration.save(); } syncThread = null; if (listener != null) { listener.syncEnded(); } } protected SyncManagerI createManager(AppSyncSource source, SyncConfig config, DeviceConfig dc) { // We must create the proper sync manager instance, depending on the // source type/properties if (source.getIsMedia()) { SapiSyncManager sm = new SapiSyncManager(config, dc); return sm; } else { // We apply some logic to decide some of the synchronization configuration properties. DevInf serverDevInf = configuration.getServerDevInf(); adaptSyncConfig(config, dc, serverDevInf); SyncManager sm = new SyncManager(config, dc); if(customTransportAgent != null) { sm.setTransportAgent(customTransportAgent); } if (customHeaders != null) { sm.addTranportAgentHeaders(customHeaders); } return sm; } } private class SyncThread extends Thread { private final Vector appSources; private boolean compressionRetry; private SyncConfig syncConfig; private DeviceConfig deviceConfig; private SyncManagerI manager; public SyncThread(Vector sources) { this.appSources = sources; this.compressionRetry = false; } public SyncConfig getSyncConfig() { return syncConfig; } public void cancelSync() { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Cancelling sync"); } if (manager != null) { manager.cancel(); } } public void run() { sync(); } public synchronized void sync() { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "SyncThread.run"); } syncConfig = configuration.getSyncConfig(); deviceConfig = configuration.getDeviceConfig(); if (compressionRetry) { // We are trying without compression, make sure compression is // really disabled syncConfig.compress = false; } if (listener != null) { listener.syncStarted(appSources); } try { synchronize(); } catch (CompressedSyncException e) { if (!compressionRetry) { // Only retry because of compression once if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Sync failed because compression failed - Retrying"); } compressionRetry = true; // Recurse this.run(); return; } } catch (Throwable e) { // This is unexpected, but we don't want the app to die Log.error(TAG_LOG, "Exception caught during synchronization", e); } finally { syncEnded(); } } /** * The main procedure for the sync thread * * @throws Exception */ private Vector synchronize() throws Exception { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "synchronize"); } Vector failedSources = new Vector(); for (int x = 0; x < appSources.size(); x++) { if (listener != null && listener.isCancelled()) { // If the sync got cancelled then we must exit the loop // Even if the user cancel the sync the SyncSource may be // unable to throw the proper exception if other errors // (such as network error) kick in. So we must recheck here // and stop the sync if necessary break; } AppSyncSource appSource = (AppSyncSource)appSources.elementAt(x); SyncSource source = appSource.getSyncSource(); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Firing sync for source " + appSource.getName()); } // We need to create one manager for each source manager = createManager(appSource, syncConfig, deviceConfig); if (listener != null) { listener.sourceStarted(appSource); } try { // Set this source as the one currently synchronized currentSource = appSource; //This sync could have been cancelled when the client was //deleting the device items during a refresh from server operation if (listener == null || (!listener.isCancelled())) { //Ask for server dev inf if this is required boolean askServerCaps = configuration.getCredentialsCheckPending() || configuration.getForceServerCapsRequest() || configuration.getServerDevInf() == null; if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Asking for server caps: " + askServerCaps); } fireSync(manager, source, source.getConfig().getSyncMode(), askServerCaps); } currentSource = null; if (source.getStatus() != SyncSource.STATUS_SUCCESS) { failedSources.addElement(appSource); } // If we get here, it means that the sync was succesfull and // we can update the anchors. A corner case is that a cancel // operation has been performed when the client was removing // all data during a refresh from server operation. In that // case the sync was never started but a slow sync must take // place next time in order not to have intem deleted whitin // a reset operation to be sent to the server on the next // fast sync if (listener != null) { listener.sourceEnded(appSource); } } catch (final Exception e) { boolean compressError = (e instanceof CompressedSyncException); if (compressError) { if (!compressionRetry) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Retrying without compression"); } } throw new CompressedSyncException(e.getMessage()); } SyncException se; if (e instanceof SyncException) { se = (SyncException)e; } else { se = new SyncException(SyncException.CLIENT_ERROR, e.toString()); } if (listener != null) { listener.sourceFailed(appSource, se); } // After this point, we know the source really // failed failedSources.addElement(appSource); // Depending on the reason why the sync failed, we may // want to abort the other sources as well (e.g. invalid // credentials) if (se.getCode() == SyncException.AUTH_ERROR || se.getCode() == SyncException.FORBIDDEN_ERROR || se.getCode() == SyncException.CANCELLED) { break; } } finally { source = null; currentSource = null; } } // -- END OF A SYNCHRONIZATION if (listener != null) { listener.endSync(appSources, failedSources.size() > 0); } return failedSources; } } protected void fireSync(SyncManagerI manager, SyncSource source, int syncMode, boolean askServerCaps) { manager.sync(source, syncMode, askServerCaps); } /** * TODO FIXME: this method shall be based on a compatibility table of some kind */ protected void adaptSyncConfig(SyncConfig syncConfig, DeviceConfig deviceConfig, DevInf serverDevInf) { // If the customization forces the usage of WBXML, then we always use it // otherwise we use it only for servers that we know are compatible with // our implementation if (customization.getUseWbxml()) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "WBXML usage is forced by Customization"); } deviceConfig.setWBXML(true); } else { if (serverDevInf != null) { String man = serverDevInf.getMan(); if (StringUtil.equalsIgnoreCase(man, "funambol")) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "WBXML enabled"); } deviceConfig.setWBXML(true); } } else { // We don't know yet who we are talking to, for this reason we try to use the most conservative // configuration if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "WBXML disabled"); } deviceConfig.setWBXML(false); } } } }