/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.content; import com.google.android.collect.Maps; import com.android.internal.R; import com.android.internal.util.ArrayUtils; import android.accounts.AccountMonitor; import android.accounts.AccountMonitorListener; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Settings; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.format.Time; import android.util.Config; import android.util.EventLog; import android.util.Log; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Random; /** * @hide */ class SyncManager { private static final String TAG = "SyncManager"; // used during dumping of the Sync history private static final long MILLIS_IN_HOUR = 1000 * 60 * 60; private static final long MILLIS_IN_DAY = MILLIS_IN_HOUR * 24; private static final long MILLIS_IN_WEEK = MILLIS_IN_DAY * 7; private static final long MILLIS_IN_4WEEKS = MILLIS_IN_WEEK * 4; /** Delay a sync due to local changes this long. In milliseconds */ private static final long LOCAL_SYNC_DELAY = 30 * 1000; // 30 seconds /** * If a sync takes longer than this and the sync queue is not empty then we will * cancel it and add it back to the end of the sync queue. In milliseconds. */ private static final long MAX_TIME_PER_SYNC = 5 * 60 * 1000; // 5 minutes private static final long SYNC_NOTIFICATION_DELAY = 30 * 1000; // 30 seconds /** * When retrying a sync for the first time use this delay. After that * the retry time will double until it reached MAX_SYNC_RETRY_TIME. * In milliseconds. */ private static final long INITIAL_SYNC_RETRY_TIME_IN_MS = 30 * 1000; // 30 seconds /** * Default the max sync retry time to this value. */ private static final long DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS = 60 * 60; // one hour /** * An error notification is sent if sync of any of the providers has been failing for this long. */ private static final long ERROR_NOTIFICATION_DELAY_MS = 1000 * 60 * 10; // 10 minutes private static final String SYNC_WAKE_LOCK = "SyncManagerSyncWakeLock"; private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarmWakeLock"; private Context mContext; private ContentResolver mContentResolver; private String mStatusText = ""; private long mHeartbeatTime = 0; private AccountMonitor mAccountMonitor; private volatile String[] mAccounts = null; volatile private PowerManager.WakeLock mSyncWakeLock; volatile private PowerManager.WakeLock mHandleAlarmWakeLock; volatile private boolean mDataConnectionIsConnected = false; volatile private boolean mStorageIsLow = false; private final NotificationManager mNotificationMgr; private AlarmManager mAlarmService = null; private HandlerThread mSyncThread; private volatile IPackageManager mPackageManager; private final SyncStorageEngine mSyncStorageEngine; private final SyncQueue mSyncQueue; private ActiveSyncContext mActiveSyncContext = null; // set if the sync error indicator should be reported. private boolean mNeedSyncErrorNotification = false; // set if the sync active indicator should be reported private boolean mNeedSyncActiveNotification = false; private volatile boolean mSyncPollInitialized; private final PendingIntent mSyncAlarmIntent; private final PendingIntent mSyncPollAlarmIntent; private BroadcastReceiver mStorageIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { ensureContentResolver(); String action = intent.getAction(); if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Internal storage is low."); } mStorageIsLow = true; cancelActiveSync(null /* no url */); } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Internal storage is ok."); } mStorageIsLow = false; sendCheckAlarmsMessage(); } } }; private BroadcastReceiver mConnectivityIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { NetworkInfo networkInfo = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); NetworkInfo.State state = (networkInfo == null ? NetworkInfo.State.UNKNOWN : networkInfo.getState()); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "received connectivity action. network info: " + networkInfo); } // only pay attention to the CONNECTED and DISCONNECTED states. // if connected, we are connected. // if disconnected, we may not be connected. in some cases, we may be connected on // a different network. // e.g., if switching from GPRS to WiFi, we may receive the CONNECTED to WiFi and // DISCONNECTED for GPRS in any order. if we receive the CONNECTED first, and then // a DISCONNECTED, we want to make sure we set mDataConnectionIsConnected to true // since we still have a WiFi connection. switch (state) { case CONNECTED: mDataConnectionIsConnected = true; break; case DISCONNECTED: if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) { mDataConnectionIsConnected = false; } else { mDataConnectionIsConnected = true; } break; default: // ignore the rest of the states -- leave our boolean alone. } if (mDataConnectionIsConnected) { initializeSyncPoll(); sendCheckAlarmsMessage(); } } }; private BroadcastReceiver mShutdownIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { Log.w(TAG, "Writing sync state before shutdown..."); getSyncStorageEngine().writeAllState(); } }; private static final String ACTION_SYNC_ALARM = "android.content.syncmanager.SYNC_ALARM"; private static final String SYNC_POLL_ALARM = "android.content.syncmanager.SYNC_POLL_ALARM"; private final SyncHandler mSyncHandler; private static final int MAX_SYNC_POLL_DELAY_SECONDS = 36 * 60 * 60; // 36 hours private static final int MIN_SYNC_POLL_DELAY_SECONDS = 24 * 60 * 60; // 24 hours private static final String SYNCMANAGER_PREFS_FILENAME = "/data/system/syncmanager.prefs"; public SyncManager(Context context, boolean factoryTest) { // Initialize the SyncStorageEngine first, before registering observers // and creating threads and so on; it may fail if the disk is full. SyncStorageEngine.init(context); mSyncStorageEngine = SyncStorageEngine.getSingleton(); mSyncQueue = new SyncQueue(mSyncStorageEngine); mContext = context; mSyncThread = new HandlerThread("SyncHandlerThread", Process.THREAD_PRIORITY_BACKGROUND); mSyncThread.start(); mSyncHandler = new SyncHandler(mSyncThread.getLooper()); mPackageManager = null; mSyncAlarmIntent = PendingIntent.getBroadcast( mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0); mSyncPollAlarmIntent = PendingIntent.getBroadcast( mContext, 0 /* ignored */, new Intent(SYNC_POLL_ALARM), 0); IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(mConnectivityIntentReceiver, intentFilter); intentFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); context.registerReceiver(mStorageIntentReceiver, intentFilter); intentFilter = new IntentFilter(Intent.ACTION_SHUTDOWN); intentFilter.setPriority(100); context.registerReceiver(mShutdownIntentReceiver, intentFilter); if (!factoryTest) { mNotificationMgr = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); context.registerReceiver(new SyncAlarmIntentReceiver(), new IntentFilter(ACTION_SYNC_ALARM)); } else { mNotificationMgr = null; } PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mSyncWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, SYNC_WAKE_LOCK); mSyncWakeLock.setReferenceCounted(false); // This WakeLock is used to ensure that we stay awake between the time that we receive // a sync alarm notification and when we finish processing it. We need to do this // because we don't do the work in the alarm handler, rather we do it in a message // handler. mHandleAlarmWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, HANDLE_SYNC_ALARM_WAKE_LOCK); mHandleAlarmWakeLock.setReferenceCounted(false); mSyncStorageEngine.addStatusChangeListener( SyncStorageEngine.CHANGE_SETTINGS, new ISyncStatusObserver.Stub() { public void onStatusChanged(int which) { // force the sync loop to run if the settings change sendCheckAlarmsMessage(); } }); if (!factoryTest) { AccountMonitorListener listener = new AccountMonitorListener() { public void onAccountsUpdated(String[] accounts) { final boolean hadAccountsAlready = mAccounts != null; // copy the accounts into a new array and change mAccounts to point to it String[] newAccounts = new String[accounts.length]; System.arraycopy(accounts, 0, newAccounts, 0, accounts.length); mAccounts = newAccounts; // if a sync is in progress yet it is no longer in the accounts list, cancel it ActiveSyncContext activeSyncContext = mActiveSyncContext; if (activeSyncContext != null) { if (!ArrayUtils.contains(newAccounts, activeSyncContext.mSyncOperation.account)) { Log.d(TAG, "canceling sync since the account has been removed"); sendSyncFinishedOrCanceledMessage(activeSyncContext, null /* no result since this is a cancel */); } } // we must do this since we don't bother scheduling alarms when // the accounts are not set yet sendCheckAlarmsMessage(); mSyncStorageEngine.doDatabaseCleanup(accounts); if (hadAccountsAlready && mAccounts.length > 0) { // request a sync so that if the password was changed we will retry any sync // that failed when it was wrong startSync(null /* all providers */, null /* no extras */); } } }; mAccountMonitor = new AccountMonitor(context, listener); } } private synchronized void initializeSyncPoll() { if (mSyncPollInitialized) return; mSyncPollInitialized = true; mContext.registerReceiver(new SyncPollAlarmReceiver(), new IntentFilter(SYNC_POLL_ALARM)); // load the next poll time from shared preferences long absoluteAlarmTime = readSyncPollTime(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "initializeSyncPoll: absoluteAlarmTime is " + absoluteAlarmTime); } // Convert absoluteAlarmTime to elapsed realtime. If this time was in the past then // schedule the poll immediately, if it is too far in the future then cap it at // MAX_SYNC_POLL_DELAY_SECONDS. long absoluteNow = System.currentTimeMillis(); long relativeNow = SystemClock.elapsedRealtime(); long relativeAlarmTime = relativeNow; if (absoluteAlarmTime > absoluteNow) { long delayInMs = absoluteAlarmTime - absoluteNow; final int maxDelayInMs = MAX_SYNC_POLL_DELAY_SECONDS * 1000; if (delayInMs > maxDelayInMs) { delayInMs = MAX_SYNC_POLL_DELAY_SECONDS * 1000; } relativeAlarmTime += delayInMs; } // schedule an alarm for the next poll time scheduleSyncPollAlarm(relativeAlarmTime); } private void scheduleSyncPollAlarm(long relativeAlarmTime) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "scheduleSyncPollAlarm: relativeAlarmTime is " + relativeAlarmTime + ", now is " + SystemClock.elapsedRealtime() + ", delay is " + (relativeAlarmTime - SystemClock.elapsedRealtime())); } ensureAlarmService(); mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, relativeAlarmTime, mSyncPollAlarmIntent); } /** * Return a random value v that satisfies minValue <= v < maxValue. The difference between * maxValue and minValue must be less than Integer.MAX_VALUE. */ private long jitterize(long minValue, long maxValue) { Random random = new Random(SystemClock.elapsedRealtime()); long spread = maxValue - minValue; if (spread > Integer.MAX_VALUE) { throw new IllegalArgumentException("the difference between the maxValue and the " + "minValue must be less than " + Integer.MAX_VALUE); } return minValue + random.nextInt((int)spread); } private void handleSyncPollAlarm() { // determine the next poll time long delayMs = jitterize(MIN_SYNC_POLL_DELAY_SECONDS, MAX_SYNC_POLL_DELAY_SECONDS) * 1000; long nextRelativePollTimeMs = SystemClock.elapsedRealtime() + delayMs; if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "handleSyncPollAlarm: delay " + delayMs); // write the absolute time to shared preferences writeSyncPollTime(System.currentTimeMillis() + delayMs); // schedule an alarm for the next poll time scheduleSyncPollAlarm(nextRelativePollTimeMs); // perform a poll scheduleSync(null /* sync all syncable providers */, new Bundle(), 0 /* no delay */); } private void writeSyncPollTime(long when) { File f = new File(SYNCMANAGER_PREFS_FILENAME); DataOutputStream str = null; try { str = new DataOutputStream(new FileOutputStream(f)); str.writeLong(when); } catch (FileNotFoundException e) { Log.w(TAG, "error writing to file " + f, e); } catch (IOException e) { Log.w(TAG, "error writing to file " + f, e); } finally { if (str != null) { try { str.close(); } catch (IOException e) { Log.w(TAG, "error closing file " + f, e); } } } } private long readSyncPollTime() { File f = new File(SYNCMANAGER_PREFS_FILENAME); DataInputStream str = null; try { str = new DataInputStream(new FileInputStream(f)); return str.readLong(); } catch (FileNotFoundException e) { writeSyncPollTime(0); } catch (IOException e) { Log.w(TAG, "error reading file " + f, e); } finally { if (str != null) { try { str.close(); } catch (IOException e) { Log.w(TAG, "error closing file " + f, e); } } } return 0; } public ActiveSyncContext getActiveSyncContext() { return mActiveSyncContext; } public SyncStorageEngine getSyncStorageEngine() { return mSyncStorageEngine; } private void ensureContentResolver() { if (mContentResolver == null) { mContentResolver = mContext.getContentResolver(); } } private void ensureAlarmService() { if (mAlarmService == null) { mAlarmService = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); } } public String getSyncingAccount() { ActiveSyncContext activeSyncContext = mActiveSyncContext; return (activeSyncContext != null) ? activeSyncContext.mSyncOperation.account : null; } /** * Returns whether or not sync is enabled. Sync can be enabled by * setting the system property "ro.config.sync" to the value "yes". * This is normally done at boot time on builds that support sync. * @return true if sync is enabled */ private boolean isSyncEnabled() { // Require the precise value "yes" to discourage accidental activation. return "yes".equals(SystemProperties.get("ro.config.sync")); } /** * Initiate a sync. This can start a sync for all providers * (pass null to url, set onlyTicklable to false), only those * providers that are marked as ticklable (pass null to url, * set onlyTicklable to true), or a specific provider (set url * to the content url of the provider). * *
If the ContentResolver.SYNC_EXTRAS_UPLOAD boolean in extras is * true then initiate a sync that just checks for local changes to send * to the server, otherwise initiate a sync that first gets any * changes from the server before sending local changes back to * the server. * *
If a specific provider is being synced (the url is non-null) * then the extras can contain SyncAdapter-specific information * to control what gets synced (e.g. which specific feed to sync). * *
You'll start getting callbacks after this.
*
* @param url The Uri of a specific provider to be synced, or
* null to sync all providers.
* @param extras a Map of SyncAdapter-specific information to control
* syncs of a specific provider. Can be null. Is ignored
* if the url is null.
* @param delay how many milliseconds in the future to wait before performing this
* sync. -1 means to make this the next sync to perform.
*/
public void scheduleSync(Uri url, Bundle extras, long delay) {
boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
if (isLoggable) {
Log.v(TAG, "scheduleSync:"
+ " delay " + delay
+ ", url " + ((url == null) ? "(null)" : url)
+ ", extras " + ((extras == null) ? "(null)" : extras));
}
if (!isSyncEnabled()) {
if (isLoggable) {
Log.v(TAG, "not syncing because sync is disabled");
}
setStatusText("Sync is disabled.");
return;
}
if (mAccounts == null) setStatusText("The accounts aren't known yet.");
if (!mDataConnectionIsConnected) setStatusText("No data connection");
if (mStorageIsLow) setStatusText("Memory low");
if (extras == null) extras = new Bundle();
Boolean expedited = extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
if (expedited) {
delay = -1; // this means schedule at the front of the queue
}
String[] accounts;
String accountFromExtras = extras.getString(ContentResolver.SYNC_EXTRAS_ACCOUNT);
if (!TextUtils.isEmpty(accountFromExtras)) {
accounts = new String[]{accountFromExtras};
} else {
// if the accounts aren't configured yet then we can't support an account-less
// sync request
accounts = mAccounts;
if (accounts == null) {
// not ready yet
if (isLoggable) {
Log.v(TAG, "scheduleSync: no accounts yet, dropping");
}
return;
}
if (accounts.length == 0) {
if (isLoggable) {
Log.v(TAG, "scheduleSync: no accounts configured, dropping");
}
setStatusText("No accounts are configured.");
return;
}
}
final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false);
final boolean force = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false);
int source;
if (uploadOnly) {
source = SyncStorageEngine.SOURCE_LOCAL;
} else if (force) {
source = SyncStorageEngine.SOURCE_USER;
} else if (url == null) {
source = SyncStorageEngine.SOURCE_POLL;
} else {
// this isn't strictly server, since arbitrary callers can (and do) request
// a non-forced two-way sync on a specific url
source = SyncStorageEngine.SOURCE_SERVER;
}
List You'll start getting callbacks after this.
*
* @param url The Uri of a specific provider to be synced, or
* null to sync all providers.
* @param extras a Map of SyncAdapter specific information to control
* syncs of a specific provider. Can be null. Is ignored
*/
public void startSync(Uri url, Bundle extras) {
scheduleSync(url, extras, 0 /* no delay */);
}
public void updateHeartbeatTime() {
mHeartbeatTime = SystemClock.elapsedRealtime();
mSyncStorageEngine.reportActiveChange();
}
private void sendSyncAlarmMessage() {
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_ALARM");
mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_SYNC_ALARM);
}
private void sendCheckAlarmsMessage() {
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_CHECK_ALARMS");
mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_CHECK_ALARMS);
}
private void sendSyncFinishedOrCanceledMessage(ActiveSyncContext syncContext,
SyncResult syncResult) {
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_FINISHED");
Message msg = mSyncHandler.obtainMessage();
msg.what = SyncHandler.MESSAGE_SYNC_FINISHED;
msg.obj = new SyncHandlerMessagePayload(syncContext, syncResult);
mSyncHandler.sendMessage(msg);
}
class SyncHandlerMessagePayload {
public final ActiveSyncContext activeSyncContext;
public final SyncResult syncResult;
SyncHandlerMessagePayload(ActiveSyncContext syncContext, SyncResult syncResult) {
this.activeSyncContext = syncContext;
this.syncResult = syncResult;
}
}
class SyncAlarmIntentReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
mHandleAlarmWakeLock.acquire();
sendSyncAlarmMessage();
}
}
class SyncPollAlarmReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
handleSyncPollAlarm();
}
}
private void rescheduleImmediately(SyncOperation syncOperation) {
SyncOperation rescheduledSyncOperation = new SyncOperation(syncOperation);
rescheduledSyncOperation.setDelay(0);
scheduleSyncOperation(rescheduledSyncOperation);
}
private long rescheduleWithDelay(SyncOperation syncOperation) {
long newDelayInMs;
if (syncOperation.delay == 0) {
// The initial delay is the jitterized INITIAL_SYNC_RETRY_TIME_IN_MS
newDelayInMs = jitterize(INITIAL_SYNC_RETRY_TIME_IN_MS,
(long)(INITIAL_SYNC_RETRY_TIME_IN_MS * 1.1));
} else {
// Subsequent delays are the double of the previous delay
newDelayInMs = syncOperation.delay * 2;
}
// Cap the delay
ensureContentResolver();
long maxSyncRetryTimeInSeconds = Settings.Gservices.getLong(mContentResolver,
Settings.Gservices.SYNC_MAX_RETRY_DELAY_IN_SECONDS,
DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS);
if (newDelayInMs > maxSyncRetryTimeInSeconds * 1000) {
newDelayInMs = maxSyncRetryTimeInSeconds * 1000;
}
SyncOperation rescheduledSyncOperation = new SyncOperation(syncOperation);
rescheduledSyncOperation.setDelay(newDelayInMs);
scheduleSyncOperation(rescheduledSyncOperation);
return newDelayInMs;
}
/**
* Cancel the active sync if it matches the uri. The uri corresponds to the one passed
* in to startSync().
* @param uri If non-null, the active sync is only canceled if it matches the uri.
* If null, any active sync is canceled.
*/
public void cancelActiveSync(Uri uri) {
ActiveSyncContext activeSyncContext = mActiveSyncContext;
if (activeSyncContext != null) {
// if a Uri was specified then only cancel the sync if it matches the the uri
if (uri != null) {
if (!uri.getAuthority().equals(activeSyncContext.mSyncOperation.authority)) {
return;
}
}
sendSyncFinishedOrCanceledMessage(activeSyncContext,
null /* no result since this is a cancel */);
}
}
/**
* Create and schedule a SyncOperation.
*
* @param syncOperation the SyncOperation to schedule
*/
public void scheduleSyncOperation(SyncOperation syncOperation) {
// If this operation is expedited and there is a sync in progress then
// reschedule the current operation and send a cancel for it.
final boolean expedited = syncOperation.delay < 0;
final ActiveSyncContext activeSyncContext = mActiveSyncContext;
if (expedited && activeSyncContext != null) {
final boolean activeIsExpedited = activeSyncContext.mSyncOperation.delay < 0;
final boolean hasSameKey =
activeSyncContext.mSyncOperation.key.equals(syncOperation.key);
// This request is expedited and there is a sync in progress.
// Interrupt the current sync only if it is not expedited and if it has a different
// key than the one we are scheduling.
if (!activeIsExpedited && !hasSameKey) {
rescheduleImmediately(activeSyncContext.mSyncOperation);
sendSyncFinishedOrCanceledMessage(activeSyncContext,
null /* no result since this is a cancel */);
}
}
boolean operationEnqueued;
synchronized (mSyncQueue) {
operationEnqueued = mSyncQueue.add(syncOperation);
}
if (operationEnqueued) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "scheduleSyncOperation: enqueued " + syncOperation);
}
sendCheckAlarmsMessage();
} else {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "scheduleSyncOperation: dropping duplicate sync operation "
+ syncOperation);
}
}
}
/**
* Remove any scheduled sync operations that match uri. The uri corresponds to the one passed
* in to startSync().
* @param uri If non-null, only operations that match the uri are cleared.
* If null, all operations are cleared.
*/
public void clearScheduledSyncOperations(Uri uri) {
synchronized (mSyncQueue) {
mSyncQueue.clear(null, uri != null ? uri.getAuthority() : null);
}
}
void maybeRescheduleSync(SyncResult syncResult, SyncOperation previousSyncOperation) {
boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG);
if (isLoggable) {
Log.d(TAG, "encountered error(s) during the sync: " + syncResult + ", "
+ previousSyncOperation);
}
// If the operation succeeded to some extent then retry immediately.
// If this was a two-way sync then retry soft errors with an exponential backoff.
// If this was an upward sync then schedule a two-way sync immediately.
// Otherwise do not reschedule.
if (syncResult.madeSomeProgress()) {
if (isLoggable) {
Log.d(TAG, "retrying sync operation immediately because "
+ "even though it had an error it achieved some success");
}
rescheduleImmediately(previousSyncOperation);
} else if (previousSyncOperation.extras.getBoolean(
ContentResolver.SYNC_EXTRAS_UPLOAD, false)) {
final SyncOperation newSyncOperation = new SyncOperation(previousSyncOperation);
newSyncOperation.extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false);
newSyncOperation.setDelay(0);
if (Config.LOGD) {
Log.d(TAG, "retrying sync operation as a two-way sync because an upload-only sync "
+ "encountered an error: " + previousSyncOperation);
}
scheduleSyncOperation(newSyncOperation);
} else if (syncResult.hasSoftError()) {
long delay = rescheduleWithDelay(previousSyncOperation);
if (delay >= 0) {
if (isLoggable) {
Log.d(TAG, "retrying sync operation in " + delay + " ms because "
+ "it encountered a soft error: " + previousSyncOperation);
}
}
} else {
if (Config.LOGD) {
Log.d(TAG, "not retrying sync operation because the error is a hard error: "
+ previousSyncOperation);
}
}
}
/**
* Value type that represents a sync operation.
*/
static class SyncOperation implements Comparable {
final String account;
int syncSource;
String authority;
Bundle extras;
final String key;
long earliestRunTime;
long delay;
SyncStorageEngine.PendingOperation pendingOperation;
SyncOperation(String account, int source, String authority, Bundle extras, long delay) {
this.account = account;
this.syncSource = source;
this.authority = authority;
this.extras = new Bundle(extras);
this.setDelay(delay);
this.key = toKey();
}
SyncOperation(SyncOperation other) {
this.account = other.account;
this.syncSource = other.syncSource;
this.authority = other.authority;
this.extras = new Bundle(other.extras);
this.delay = other.delay;
this.earliestRunTime = other.earliestRunTime;
this.key = toKey();
}
public void setDelay(long delay) {
this.delay = delay;
if (delay >= 0) {
this.earliestRunTime = SystemClock.elapsedRealtime() + delay;
} else {
this.earliestRunTime = 0;
}
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("authority: ").append(authority);
sb.append(" account: ").append(account);
sb.append(" extras: ");
extrasToStringBuilder(extras, sb);
sb.append(" syncSource: ").append(syncSource);
sb.append(" when: ").append(earliestRunTime);
sb.append(" delay: ").append(delay);
sb.append(" key: {").append(key).append("}");
if (pendingOperation != null) sb.append(" pendingOperation: ").append(pendingOperation);
return sb.toString();
}
private String toKey() {
StringBuilder sb = new StringBuilder();
sb.append("authority: ").append(authority);
sb.append(" account: ").append(account);
sb.append(" extras: ");
extrasToStringBuilder(extras, sb);
return sb.toString();
}
private static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
sb.append("[");
for (String key : bundle.keySet()) {
sb.append(key).append("=").append(bundle.get(key)).append(" ");
}
sb.append("]");
}
public int compareTo(Object o) {
SyncOperation other = (SyncOperation)o;
if (earliestRunTime == other.earliestRunTime) {
return 0;
}
return (earliestRunTime < other.earliestRunTime) ? -1 : 1;
}
}
/**
* @hide
*/
class ActiveSyncContext extends ISyncContext.Stub {
final SyncOperation mSyncOperation;
final long mHistoryRowId;
final IContentProvider mContentProvider;
final ISyncAdapter mSyncAdapter;
final long mStartTime;
long mTimeoutStartTime;
public ActiveSyncContext(SyncOperation syncOperation, IContentProvider contentProvider,
ISyncAdapter syncAdapter, long historyRowId) {
super();
mSyncOperation = syncOperation;
mHistoryRowId = historyRowId;
mContentProvider = contentProvider;
mSyncAdapter = syncAdapter;
mStartTime = SystemClock.elapsedRealtime();
mTimeoutStartTime = mStartTime;
}
public void sendHeartbeat() {
// ignore this call if it corresponds to an old sync session
if (mActiveSyncContext == this) {
SyncManager.this.updateHeartbeatTime();
}
}
public void onFinished(SyncResult result) {
// include "this" in the message so that the handler can ignore it if this
// ActiveSyncContext is no longer the mActiveSyncContext at message handling
// time
sendSyncFinishedOrCanceledMessage(this, result);
}
public void toString(StringBuilder sb) {
sb.append("startTime ").append(mStartTime)
.append(", mTimeoutStartTime ").append(mTimeoutStartTime)
.append(", mHistoryRowId ").append(mHistoryRowId)
.append(", syncOperation ").append(mSyncOperation);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
toString(sb);
return sb.toString();
}
}
protected void dump(FileDescriptor fd, PrintWriter pw) {
StringBuilder sb = new StringBuilder();
dumpSyncState(pw, sb);
if (isSyncEnabled()) {
dumpSyncHistory(pw, sb);
}
}
static String formatTime(long time) {
Time tobj = new Time();
tobj.set(time);
return tobj.format("%Y-%m-%d %H:%M:%S");
}
protected void dumpSyncState(PrintWriter pw, StringBuilder sb) {
pw.print("sync enabled: "); pw.println(isSyncEnabled());
pw.print("data connected: "); pw.println(mDataConnectionIsConnected);
pw.print("memory low: "); pw.println(mStorageIsLow);
final String[] accounts = mAccounts;
pw.print("accounts: ");
if (accounts != null) {
pw.println(accounts.length);
} else {
pw.println("none");
}
final long now = SystemClock.elapsedRealtime();
pw.print("now: "); pw.println(now);
pw.print("uptime: "); pw.print(DateUtils.formatElapsedTime(now/1000));
pw.println(" (HH:MM:SS)");
pw.print("time spent syncing: ");
pw.print(DateUtils.formatElapsedTime(
mSyncHandler.mSyncTimeTracker.timeSpentSyncing() / 1000));
pw.print(" (HH:MM:SS), sync ");
pw.print(mSyncHandler.mSyncTimeTracker.mLastWasSyncing ? "" : "not ");
pw.println("in progress");
if (mSyncHandler.mAlarmScheduleTime != null) {
pw.print("next alarm time: "); pw.print(mSyncHandler.mAlarmScheduleTime);
pw.print(" (");
pw.print(DateUtils.formatElapsedTime((mSyncHandler.mAlarmScheduleTime-now)/1000));
pw.println(" (HH:MM:SS) from now)");
} else {
pw.println("no alarm is scheduled (there had better not be any pending syncs)");
}
pw.print("active sync: "); pw.println(mActiveSyncContext);
pw.print("notification info: ");
sb.setLength(0);
mSyncHandler.mSyncNotificationInfo.toString(sb);
pw.println(sb.toString());
synchronized (mSyncQueue) {
pw.print("sync queue: ");
sb.setLength(0);
mSyncQueue.dump(sb);
pw.println(sb.toString());
}
ActiveSyncInfo active = mSyncStorageEngine.getActiveSync();
if (active != null) {
SyncStorageEngine.AuthorityInfo authority
= mSyncStorageEngine.getAuthority(active.authorityId);
final long durationInSeconds = (now - active.startTime) / 1000;
pw.print("Active sync: ");
pw.print(authority != null ? authority.account : "