/* * Funambol is a mobile platform developed by Funambol, Inc. * Copyright (C) 2010 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 de.chbosync.android.syncmlclient.services; import java.util.Hashtable; import java.util.Vector; import android.app.AlarmManager; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.net.Uri; import android.os.Binder; import android.os.IBinder; import com.funambol.client.configuration.Configuration; import com.funambol.client.controller.Controller; import com.funambol.client.controller.SynchronizationController; import com.funambol.client.source.AppSyncSource; import com.funambol.client.source.AppSyncSourceManager; import com.funambol.sync.SyncSource; import com.funambol.sync.client.ChangesTracker; import com.funambol.sync.client.TrackableSyncSource; import com.funambol.util.Log; import de.chbosync.android.syncmlclient.AndroidAppSyncSource; import de.chbosync.android.syncmlclient.AndroidConfiguration; import de.chbosync.android.syncmlclient.AndroidCustomization; import de.chbosync.android.syncmlclient.App; import de.chbosync.android.syncmlclient.AppInitializer; import de.chbosync.android.syncmlclient.controller.AndroidHomeScreenController; import de.chbosync.android.syncmlclient.source.AndroidChangesTracker; /** * This class is responsible for handling automatic synchronizations. In the * client there are different types of automatic syncs: * * 1) scheduled syncs (periodic) * 2) pending syncs waiting for some triggering event (for example syncs pending * on the WiFi availability) * 3) syncs pushed from the server */ public class AutoSyncService extends Service { private final String TAG = "AutoSyncService"; public static final String AUTO_SYNC_ACTION = "de.chbosync.android.syncmlclient.services.AUTO_SYNC"; public static final String OPERATION = "OPERATION"; public static final String PROGRAM_SCHEDULED_SYNC = "de.chbosync.android.syncmlclient.PROGRAM_SCHEDULED_SYNC"; public static final String CANCEL_SCHEDULED_SYNC = "de.chbosync.android.syncmlclient.CANCEL_SCHEDULED_SYNC"; public static final String PROGRAM_SYNC_RETRY = "de.chbosync.android.syncmlclient.PROGRAM_SYNC_RETRY"; public static final String CANCEL_SYNC_RETRY = "de.chbosync.android.syncmlclient.CANCEL_SYNC_RETRY"; public static final String START_MONITORING_URI = "de.chbosync.android.syncmlclient.START_MONITORING_URI"; public static final String START_SYNC = "de.chbosync.android.syncmlclient.START_SYNC"; public static final String SOURCE_ID = "SOURCE_ID"; public static final String SOURCES_ID = "SOURCES_ID"; public static final String DELAY = "DELAY"; public static final String COUNT = "COUNT"; public static final String URI = "URI"; public static final String SYNC_MODE = "SYNC_MODE"; public static final String DIRECTORY = "DIRECTORY"; public static final String EXTENSIONS = "EXTENSIONS"; public static final String EXTRA_RETRY_COUNT = "RETRY_COUNT"; private Context context; private PendingIntent scheduledSyncIntent; private PendingIntent syncRetryIntent; private AndroidConfiguration configuration; private AndroidCustomization customization; private AppSyncSourceManager appSyncSourceManager; private AlarmManager alarmManager; private AppInitializer initializer; private Hashtable <Uri,AndroidAppSyncSource> monitoredUris = new Hashtable<Uri,AndroidAppSyncSource>(); private Hashtable fileObservers = new Hashtable(); public AutoSyncService() { super(); } @Override public void onCreate() { super.onCreate(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG, "Service Created"); } initializer = App.i().getAppInitializer(); initializer.init(); configuration = initializer.getConfiguration(); customization = AndroidCustomization.getInstance(); appSyncSourceManager = initializer.getAppSyncSourceManager(); context = getApplicationContext(); } @Override public IBinder onBind(Intent intent) { startForeground(Notification.FLAG_FOREGROUND_SERVICE, new Notification(0, null, System.currentTimeMillis())); return new AutoSyncBinder(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG, "Service Started"); } String operation = null; if(intent != null) { operation = intent.getStringExtra(OPERATION); } if (START_MONITORING_URI.equals(operation)) { int sourceId = intent.getIntExtra(SOURCE_ID, -1); String uri = intent.getStringExtra(URI); startMonitoringUri(uri, sourceId); } else if (START_SYNC.equals(operation)) { String syncMode = intent.getStringExtra(SYNC_MODE); int sourcesId[] = intent.getIntArrayExtra(SOURCES_ID); int delay = intent.getIntExtra(DELAY, 0); startSync(syncMode, sourcesId, delay); } else if (PROGRAM_SCHEDULED_SYNC.equals(operation)) { programScheduledSync(); } else if (CANCEL_SCHEDULED_SYNC.equals(operation)) { cancelScheduledSync(); } else if (PROGRAM_SYNC_RETRY.equals(operation)) { int delay = intent.getIntExtra(DELAY, 0); int count = intent.getIntExtra(COUNT, 0); programSingleSync(delay, count); } else if (CANCEL_SYNC_RETRY.equals(operation)) { cancelSyncRetry(); } else if (operation == null) { startForeground(Notification.FLAG_FOREGROUND_SERVICE, new Notification(0, null, System.currentTimeMillis())); // Program scheduled sync if necessary if(!configuration.getCredentialsCheckPending()) { programScheduledSync(); } // Program sync retry if necessary if(customization.getSyncRetryEnabled()) { int retryCount = configuration.getCurrentSyncRetryCount(); if(retryCount >= 0) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG, "A previous sync (retry) didn't end correctly. " + "Retry in 10 seconds"); } programSingleSyncSeconds(10, retryCount); } } } return START_STICKY; } private void programScheduledSync() { // Check in the configuration if a form of auto synchronization is set if (configuration.getSyncMode() == Configuration.SYNC_MODE_SCHEDULED) { int interval = configuration.getPollingInterval(); if (interval > 0) { // Scheduled sync if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG, "Programming scheduled sync at " + interval); } programRepeatingSync(interval); } } } @Override public void onDestroy() { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG, "Service Stopped"); } } /** * LocalBinder used by activities that wish to be notified of the sync events. */ public class AutoSyncBinder extends Binder { public void startMonitoringUri(String u, int sourceId) { AutoSyncService.this.startMonitoringUri(u, sourceId); } public void programScheduledSync() { AutoSyncService.this.programScheduledSync(); } public void programSyncRetry(int minutes, int retryCount) { AutoSyncService.this.programSingleSync(minutes, retryCount); } public void cancelScheduledSync() { AutoSyncService.this.cancelScheduledSync(); } public void cancelSyncRetry() { AutoSyncService.this.cancelSyncRetry(); } } private void startMonitoringUri(String u, int sourceId) { AndroidAppSyncSource appSource = (AndroidAppSyncSource)appSyncSourceManager.getSource(sourceId); Uri uri = Uri.parse(u); ContentResolver contentResolver = context.getContentResolver(); AppSyncSource s = AutoSyncService.this.monitoredUris.get(uri); if (s == null) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG, "Start monitoring uri " + uri); } monitoredUris.put(uri, appSource); contentResolver.registerContentObserver(uri, true, new AndroidContentObserver(appSource, uri)); } } private void programRepeatingSync(int minutes) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG, "Programming repeating sync with interval: " + minutes); } int interval = minutes * 1000 * 60; Intent i = new Intent(AUTO_SYNC_ACTION); scheduledSyncIntent = PendingIntent.getBroadcast(context, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + interval, interval, scheduledSyncIntent); } private void programSingleSyncSeconds(int seconds, int retryCount) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG, "Programming sync with delay: " + seconds); } int interval = seconds * 1000; Intent i = new Intent(AUTO_SYNC_ACTION); i.putExtra(EXTRA_RETRY_COUNT, retryCount); syncRetryIntent = PendingIntent.getBroadcast(context, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + interval, syncRetryIntent); configuration.setCurrentSyncRetryCount(retryCount); configuration.save(); } private void programSingleSync(int minutes, int retryCount) { int seconds = minutes * 60; programSingleSyncSeconds(seconds, retryCount); } private void cancelScheduledSync() { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG, "cancelScheduledSync"); } if (alarmManager != null) { alarmManager.cancel(scheduledSyncIntent); } } private void cancelSyncRetry() { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG, "cancelSyncRetry"); } if (alarmManager != null) { alarmManager.cancel(syncRetryIntent); } } private void startSync(String syncMode, int sourcesId[], int delay) { Controller controller = initializer.getController(); AndroidHomeScreenController hsc = (AndroidHomeScreenController)controller.getHomeScreenController(); Vector sources = new Vector(); for(int i=0;i<sourcesId.length;++i) { AndroidAppSyncSource appSource = (AndroidAppSyncSource)appSyncSourceManager.getSource(sourcesId[i]); sources.addElement(appSource); } if(delay > 0) { hsc.fireSynchronization(syncMode, sources, delay); } else { hsc.fireSynchronization(syncMode, sources); } } /** * Implements a <code>ContentObserver</code> in order to keep track of * changes done on specific sources. */ private class AndroidContentObserver extends ContentObserver { private static final String TAG_LOG = "AndroidContentObserver"; private AndroidAppSyncSource appSource; private Uri uri; public AndroidContentObserver(AndroidAppSyncSource appSource, Uri uri) { super(null); this.appSource = appSource; this.uri = uri; } @Override public void onChange(boolean selfChange) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Detected change for uri: " + uri); } if(!configuration.isC2SPushEnabled()) { if(Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "C2S push is not enabled"); } return; } Controller controller = initializer.getController(); AndroidHomeScreenController hsc = (AndroidHomeScreenController)controller.getHomeScreenController(); if (hsc.isSynchronizing() && hsc.getCurrentSource() == appSource) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Ignoring change during sync for " + appSource.getId()); } } else { // Does this change belong to us? boolean checked = false; boolean hasChanges = true; SyncSource ss = appSource.getSyncSource(); if (ss instanceof TrackableSyncSource) { ChangesTracker tracker = ((TrackableSyncSource)ss).getTracker(); if (tracker instanceof ChangesTracker) { hasChanges = ((AndroidChangesTracker)tracker).hasChanges(); checked = true; } } if (!checked) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Cannot check if change is on our account, schedule a sync"); } hasChanges = true; } if (hasChanges) { Vector sources = new Vector(); sources.addElement(appSource); int delay = customization.getC2SPushDelay(); AutoSyncServiceHandler autoSyncHandler = new AutoSyncServiceHandler(context); autoSyncHandler.startSync(SynchronizationController.PUSH, sources, delay); } } } } }