AnonymousSyncService & SyncRequest.
Changes to the way bundles are parcelled broke SM,
this update writes out the bundle as xml. This circumvents
the need for parcel, and makes it easier to debug whats
happening.
Change-Id: I6cd5d3a2eb80bfa5b3ae0c7f2d2ff91a65daaa34
diff --git a/services/java/com/android/server/content/ContentService.java b/services/java/com/android/server/content/ContentService.java
index 4a5c0d5..a56af08 100644
--- a/services/java/com/android/server/content/ContentService.java
+++ b/services/java/com/android/server/content/ContentService.java
@@ -19,6 +19,7 @@
import android.Manifest;
import android.accounts.Account;
import android.app.ActivityManager;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentService;
@@ -26,6 +27,7 @@
import android.content.PeriodicSync;
import android.content.SyncAdapterType;
import android.content.SyncInfo;
+import android.content.SyncRequest;
import android.content.SyncStatusInfo;
import android.database.IContentObserver;
import android.database.sqlite.SQLiteException;
@@ -39,6 +41,7 @@
import android.os.SystemProperties;
import android.os.UserHandle;
import android.util.Log;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseIntArray;
@@ -312,6 +315,7 @@
}
}
+ @Override
public void requestSync(Account account, String authority, Bundle extras) {
ContentResolver.validateSyncExtrasBundle(extras);
int userId = UserHandle.getCallingUserId();
@@ -323,7 +327,8 @@
try {
SyncManager syncManager = getSyncManager();
if (syncManager != null) {
- syncManager.scheduleSync(account, userId, uId, authority, extras, 0 /* no delay */,
+ syncManager.scheduleSync(account, userId, uId, authority, extras,
+ 0 /* no delay */, 0 /* no delay */,
false /* onlyThoseWithUnkownSyncableState */);
}
} finally {
@@ -332,11 +337,83 @@
}
/**
+ * Request a sync with a generic {@link android.content.SyncRequest} object. This will be
+ * either:
+ * periodic OR one-off sync.
+ * and
+ * anonymous OR provider sync.
+ * Depending on the request, we enqueue to suit in the SyncManager.
+ * @param request
+ */
+ @Override
+ public void sync(SyncRequest request) {
+ Bundle extras = request.getBundle();
+ ContentResolver.validateSyncExtrasBundle(extras);
+
+ long flextime = request.getSyncFlexTime();
+ long runAtTime = request.getSyncRunTime();
+ int userId = UserHandle.getCallingUserId();
+ int uId = Binder.getCallingUid();
+
+ // This makes it so that future permission checks will be in the context of this
+ // process rather than the caller's process. We will restore this before returning.
+ long identityToken = clearCallingIdentity();
+ try {
+ SyncManager syncManager = getSyncManager();
+ if (syncManager != null) {
+ if (request.hasAuthority()) {
+ // Sync Adapter registered with the system - old API.
+ final Account account = request.getProviderInfo().first;
+ final String provider = request.getProviderInfo().second;
+ if (request.isPeriodic()) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.WRITE_SYNC_SETTINGS,
+ "no permission to write the sync settings");
+ if (runAtTime < 60) {
+ Slog.w(TAG, "Requested poll frequency of " + runAtTime
+ + " seconds being rounded up to 60 seconds.");
+ runAtTime = 60;
+ }
+ PeriodicSync syncToAdd =
+ new PeriodicSync(account, provider, extras, runAtTime, flextime);
+ getSyncManager().getSyncStorageEngine().addPeriodicSync(syncToAdd, userId);
+ } else {
+ long beforeRuntimeMillis = (flextime) * 1000;
+ long runtimeMillis = runAtTime * 1000;
+ syncManager.scheduleSync(
+ account, userId, uId, provider, extras,
+ beforeRuntimeMillis, runtimeMillis,
+ false /* onlyThoseWithUnknownSyncableState */);
+ }
+ } else {
+ // Anonymous sync - new API.
+ final ComponentName syncService = request.getService();
+ if (request.isPeriodic()) {
+ throw new RuntimeException("Periodic anonymous syncs not implemented yet.");
+ } else {
+ long beforeRuntimeMillis = (flextime) * 1000;
+ long runtimeMillis = runAtTime * 1000;
+ syncManager.scheduleSync(
+ syncService, userId, uId, extras,
+ beforeRuntimeMillis,
+ runtimeMillis,
+ false /* onlyThoseWithUnknownSyncableState */); // Empty function.
+ throw new RuntimeException("One-off anonymous syncs not implemented yet.");
+ }
+ }
+ }
+ } finally {
+ restoreCallingIdentity(identityToken);
+ }
+ }
+
+ /**
* Clear all scheduled sync operations that match the uri and cancel the active sync
* if they match the authority and account, if they are present.
* @param account filter the pending and active syncs to cancel using this account
* @param authority filter the pending and active syncs to cancel using this authority
*/
+ @Override
public void cancelSync(Account account, String authority) {
int userId = UserHandle.getCallingUserId();
@@ -358,6 +435,7 @@
* Get information about the SyncAdapters that are known to the system.
* @return an array of SyncAdapters that have registered with the system
*/
+ @Override
public SyncAdapterType[] getSyncAdapterTypes() {
// This makes it so that future permission checks will be in the context of this
// process rather than the caller's process. We will restore this before returning.
@@ -371,6 +449,7 @@
}
}
+ @Override
public boolean getSyncAutomatically(Account account, String providerName) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
"no permission to read the sync settings");
@@ -389,6 +468,7 @@
return false;
}
+ @Override
public void setSyncAutomatically(Account account, String providerName, boolean sync) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
"no permission to write the sync settings");
@@ -406,6 +486,10 @@
}
}
+ /**
+ * Old API. Schedule periodic sync with default flex time.
+ */
+ @Override
public void addPeriodicSync(Account account, String authority, Bundle extras,
long pollFrequency) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
@@ -420,13 +504,18 @@
long identityToken = clearCallingIdentity();
try {
- getSyncManager().getSyncStorageEngine().addPeriodicSync(
- account, userId, authority, extras, pollFrequency);
+ // Add default flex time to this sync.
+ PeriodicSync syncToAdd =
+ new PeriodicSync(account, authority, extras,
+ pollFrequency,
+ SyncStorageEngine.calculateDefaultFlexTime(pollFrequency));
+ getSyncManager().getSyncStorageEngine().addPeriodicSync(syncToAdd, userId);
} finally {
restoreCallingIdentity(identityToken);
}
}
+ @Override
public void removePeriodicSync(Account account, String authority, Bundle extras) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
"no permission to write the sync settings");
@@ -434,13 +523,23 @@
long identityToken = clearCallingIdentity();
try {
- getSyncManager().getSyncStorageEngine().removePeriodicSync(account, userId, authority,
- extras);
+ PeriodicSync syncToRemove = new PeriodicSync(account, authority, extras,
+ 0 /* Not read for removal */, 0 /* Not read for removal */);
+ getSyncManager().getSyncStorageEngine().removePeriodicSync(syncToRemove, userId);
} finally {
restoreCallingIdentity(identityToken);
}
}
+ /**
+ * TODO: Implement.
+ * @param request Sync to remove.
+ */
+ public void removeSync(SyncRequest request) {
+
+ }
+
+ @Override
public List<PeriodicSync> getPeriodicSyncs(Account account, String providerName) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
"no permission to read the sync settings");
@@ -473,6 +572,7 @@
return -1;
}
+ @Override
public void setIsSyncable(Account account, String providerName, int syncable) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
"no permission to write the sync settings");
@@ -490,6 +590,7 @@
}
}
+ @Override
public boolean getMasterSyncAutomatically() {
mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
"no permission to read the sync settings");
@@ -507,6 +608,7 @@
return false;
}
+ @Override
public void setMasterSyncAutomatically(boolean flag) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
"no permission to write the sync settings");
diff --git a/services/java/com/android/server/content/SyncManager.java b/services/java/com/android/server/content/SyncManager.java
index 2da95c3..ee5b890 100644
--- a/services/java/com/android/server/content/SyncManager.java
+++ b/services/java/com/android/server/content/SyncManager.java
@@ -34,6 +34,7 @@
import android.content.ISyncStatusObserver;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.PeriodicSync;
import android.content.ServiceConnection;
import android.content.SyncActivityTooManyDeletes;
import android.content.SyncAdapterType;
@@ -83,6 +84,7 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -190,6 +192,7 @@
private BroadcastReceiver mStorageIntentReceiver =
new BroadcastReceiver() {
+ @Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
@@ -210,36 +213,39 @@
};
private BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() {
+ @Override
public void onReceive(Context context, Intent intent) {
mSyncHandler.onBootCompleted();
}
};
private BroadcastReceiver mBackgroundDataSettingChanged = new BroadcastReceiver() {
+ @Override
public void onReceive(Context context, Intent intent) {
if (getConnectivityManager().getBackgroundDataSetting()) {
scheduleSync(null /* account */, UserHandle.USER_ALL,
SyncOperation.REASON_BACKGROUND_DATA_SETTINGS_CHANGED,
null /* authority */,
- new Bundle(), 0 /* delay */,
+ new Bundle(), 0 /* delay */, 0 /* delay */,
false /* onlyThoseWithUnknownSyncableState */);
}
}
};
private BroadcastReceiver mAccountsUpdatedReceiver = new BroadcastReceiver() {
+ @Override
public void onReceive(Context context, Intent intent) {
updateRunningAccounts();
// Kick off sync for everyone, since this was a radical account change
scheduleSync(null, UserHandle.USER_ALL, SyncOperation.REASON_ACCOUNTS_UPDATED, null,
- null, 0 /* no delay */, false);
+ null, 0 /* no delay */, 0/* no delay */, false);
}
};
private final PowerManager mPowerManager;
- // Use this as a random offset to seed all periodic syncs
+ // Use this as a random offset to seed all periodic syncs.
private int mSyncRandomOffsetMillis;
private final UserManager mUserManager;
@@ -296,6 +302,7 @@
private BroadcastReceiver mConnectivityIntentReceiver =
new BroadcastReceiver() {
+ @Override
public void onReceive(Context context, Intent intent) {
final boolean wasConnected = mDataConnectionIsConnected;
@@ -321,6 +328,7 @@
private BroadcastReceiver mShutdownIntentReceiver =
new BroadcastReceiver() {
+ @Override
public void onReceive(Context context, Intent intent) {
Log.w(TAG, "Writing sync state before shutdown...");
getSyncStorageEngine().writeAllState();
@@ -371,9 +379,13 @@
SyncStorageEngine.init(context);
mSyncStorageEngine = SyncStorageEngine.getSingleton();
mSyncStorageEngine.setOnSyncRequestListener(new OnSyncRequestListener() {
+ @Override
public void onSyncRequest(Account account, int userId, int reason, String authority,
Bundle extras) {
- scheduleSync(account, userId, reason, authority, extras, 0, false);
+ scheduleSync(account, userId, reason, authority, extras,
+ 0 /* no delay */,
+ 0 /* no delay */,
+ false);
}
});
@@ -388,7 +400,7 @@
if (!removed) {
scheduleSync(null, UserHandle.USER_ALL,
SyncOperation.REASON_SERVICE_CHANGED,
- type.authority, null, 0 /* no delay */,
+ type.authority, null, 0 /* no delay */, 0 /* no delay */,
false /* onlyThoseWithUnkownSyncableState */);
}
}
@@ -453,6 +465,7 @@
mSyncStorageEngine.addStatusChangeListener(
ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, new ISyncStatusObserver.Stub() {
+ @Override
public void onStatusChanged(int which) {
// force the sync loop to run if the settings change
sendCheckAlarmsMessage();
@@ -526,58 +539,35 @@
}
/**
- * 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).
- *
- * <p>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.
- *
- * <p>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).
- *
- * <p>You'll start getting callbacks after this.
- *
- * @param requestedAccount the account to sync, may be null to signify all accounts
+ * Initiate a sync using the new anonymous service API.
+ * TODO: Implement.
+ * @param cname SyncService component bound to in order to perform the sync.
* @param userId the id of the user whose accounts are to be synced. If userId is USER_ALL,
* then all users' accounts are considered.
- * @param reason for sync request. If this is a positive integer, it is the Linux uid
- * assigned to the process that requested the sync. If it's negative, the sync was requested by
- * the SyncManager itself and could be one of the following:
- * {@link SyncOperation#REASON_BACKGROUND_DATA_SETTINGS_CHANGED}
- * {@link SyncOperation#REASON_ACCOUNTS_UPDATED}
- * {@link SyncOperation#REASON_SERVICE_CHANGED}
- * {@link SyncOperation#REASON_PERIODIC}
- * {@link SyncOperation#REASON_IS_SYNCABLE}
- * {@link SyncOperation#REASON_SYNC_AUTO}
- * {@link SyncOperation#REASON_MASTER_SYNC_AUTO}
- * {@link SyncOperation#REASON_USER_START}
- * @param requestedAuthority the authority to sync, may be null to indicate all authorities
+ * @param uid Linux uid of the application that is performing the sync.
* @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
- * @param onlyThoseWithUnkownSyncableState
+ * syncs of a specific provider. Can be null.
+ * @param beforeRunTimeMillis
+ * @param runtimeMillis
*/
- public void scheduleSync(Account requestedAccount, int userId, int reason,
- String requestedAuthority, Bundle extras, long delay,
- boolean onlyThoseWithUnkownSyncableState) {
+ public void scheduleSync(ComponentName cname, int userId, int uid, Bundle extras,
+ long beforeRunTimeMillis, long runtimeMillis,
+ boolean onlyThoseWithUnknownSyncableState) {
+/**
boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
final boolean backgroundDataUsageAllowed = !mBootCompleted ||
getConnectivityManager().getBackgroundDataSetting();
- if (extras == null) extras = new Bundle();
-
+ if (extras == null) {
+ extras = new Bundle();
+ }
+ if (isLoggable) {
+ Log.e(TAG, requestedAccount + " " + extras.toString() + " " + requestedAuthority);
+ }
Boolean expedited = extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
if (expedited) {
- delay = -1; // this means schedule at the front of the queue
+ runtimeMillis = -1; // this means schedule at the front of the queue
}
AccountAndUser[] accounts;
@@ -682,11 +672,13 @@
account.userId, authority);
final long backoffTime = backoff != null ? backoff.first : 0;
if (isSyncable < 0) {
+ // Initialisation sync.
Bundle newExtras = new Bundle();
newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
if (isLoggable) {
- Log.v(TAG, "scheduleSync:"
- + " delay " + delay
+ Log.v(TAG, "schedule initialisation Sync:"
+ + ", delay until " + delayUntil
+ + ", run by " + 0
+ ", source " + source
+ ", account " + account
+ ", authority " + authority
@@ -694,13 +686,15 @@
}
scheduleSyncOperation(
new SyncOperation(account.account, account.userId, reason, source,
- authority, newExtras, 0, backoffTime, delayUntil,
- allowParallelSyncs));
+ authority, newExtras, 0 /* immediate , 0 /* No flex time,
+ backoffTime, delayUntil, allowParallelSyncs));
}
if (!onlyThoseWithUnkownSyncableState) {
if (isLoggable) {
Log.v(TAG, "scheduleSync:"
- + " delay " + delay
+ + " delay until " + delayUntil
+ + " run by " + runtimeMillis
+ + " flex " + beforeRuntimeMillis
+ ", source " + source
+ ", account " + account
+ ", authority " + authority
@@ -708,17 +702,223 @@
}
scheduleSyncOperation(
new SyncOperation(account.account, account.userId, reason, source,
- authority, extras, delay, backoffTime, delayUntil,
- allowParallelSyncs));
+ authority, extras, runtimeMillis, beforeRuntimeMillis,
+ backoffTime, delayUntil, allowParallelSyncs));
+ }
+ }
+ }*/
+ }
+
+ /**
+ * 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).
+ *
+ * <p>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.
+ *
+ * <p>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).
+ *
+ * <p>You'll start getting callbacks after this.
+ *
+ * @param requestedAccount the account to sync, may be null to signify all accounts
+ * @param userId the id of the user whose accounts are to be synced. If userId is USER_ALL,
+ * then all users' accounts are considered.
+ * @param reason for sync request. If this is a positive integer, it is the Linux uid
+ * assigned to the process that requested the sync. If it's negative, the sync was requested by
+ * the SyncManager itself and could be one of the following:
+ * {@link SyncOperation#REASON_BACKGROUND_DATA_SETTINGS_CHANGED}
+ * {@link SyncOperation#REASON_ACCOUNTS_UPDATED}
+ * {@link SyncOperation#REASON_SERVICE_CHANGED}
+ * {@link SyncOperation#REASON_PERIODIC}
+ * {@link SyncOperation#REASON_IS_SYNCABLE}
+ * {@link SyncOperation#REASON_SYNC_AUTO}
+ * {@link SyncOperation#REASON_MASTER_SYNC_AUTO}
+ * {@link SyncOperation#REASON_USER_START}
+ * @param requestedAuthority the authority to sync, may be null to indicate all authorities
+ * @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 beforeRuntimeMillis milliseconds before runtimeMillis that this sync can run.
+ * @param runtimeMillis maximum milliseconds in the future to wait before performing sync.
+ * @param onlyThoseWithUnkownSyncableState Only sync authorities that have unknown state.
+ */
+ public void scheduleSync(Account requestedAccount, int userId, int reason,
+ String requestedAuthority, Bundle extras, long beforeRuntimeMillis,
+ long runtimeMillis, boolean onlyThoseWithUnkownSyncableState) {
+ boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+
+ final boolean backgroundDataUsageAllowed = !mBootCompleted ||
+ getConnectivityManager().getBackgroundDataSetting();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+ if (isLoggable) {
+ Log.d(TAG, "one-time sync for: " + requestedAccount + " " + extras.toString() + " "
+ + requestedAuthority);
+ }
+ Boolean expedited = extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
+ if (expedited) {
+ runtimeMillis = -1; // this means schedule at the front of the queue
+ }
+
+ AccountAndUser[] accounts;
+ if (requestedAccount != null && userId != UserHandle.USER_ALL) {
+ accounts = new AccountAndUser[] { new AccountAndUser(requestedAccount, userId) };
+ } else {
+ // if the accounts aren't configured yet then we can't support an account-less
+ // sync request
+ accounts = mRunningAccounts;
+ if (accounts.length == 0) {
+ if (isLoggable) {
+ Log.v(TAG, "scheduleSync: no accounts configured, dropping");
+ }
+ return;
+ }
+ }
+
+ final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false);
+ final boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
+ if (manualSync) {
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true);
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
+ }
+ final boolean ignoreSettings =
+ extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false);
+
+ int source;
+ if (uploadOnly) {
+ source = SyncStorageEngine.SOURCE_LOCAL;
+ } else if (manualSync) {
+ source = SyncStorageEngine.SOURCE_USER;
+ } else if (requestedAuthority == 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;
+ }
+
+ for (AccountAndUser account : accounts) {
+ // Compile a list of authorities that have sync adapters.
+ // For each authority sync each account that matches a sync adapter.
+ final HashSet<String> syncableAuthorities = new HashSet<String>();
+ for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapter :
+ mSyncAdapters.getAllServices(account.userId)) {
+ syncableAuthorities.add(syncAdapter.type.authority);
+ }
+
+ // if the url was specified then replace the list of authorities
+ // with just this authority or clear it if this authority isn't
+ // syncable
+ if (requestedAuthority != null) {
+ final boolean hasSyncAdapter = syncableAuthorities.contains(requestedAuthority);
+ syncableAuthorities.clear();
+ if (hasSyncAdapter) syncableAuthorities.add(requestedAuthority);
+ }
+
+ for (String authority : syncableAuthorities) {
+ int isSyncable = getIsSyncable(account.account, account.userId,
+ authority);
+ if (isSyncable == 0) {
+ continue;
+ }
+ final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo;
+ syncAdapterInfo = mSyncAdapters.getServiceInfo(
+ SyncAdapterType.newKey(authority, account.account.type), account.userId);
+ if (syncAdapterInfo == null) {
+ continue;
+ }
+ final boolean allowParallelSyncs = syncAdapterInfo.type.allowParallelSyncs();
+ final boolean isAlwaysSyncable = syncAdapterInfo.type.isAlwaysSyncable();
+ if (isSyncable < 0 && isAlwaysSyncable) {
+ mSyncStorageEngine.setIsSyncable(account.account, account.userId, authority, 1);
+ isSyncable = 1;
+ }
+ if (onlyThoseWithUnkownSyncableState && isSyncable >= 0) {
+ continue;
+ }
+ if (!syncAdapterInfo.type.supportsUploading() && uploadOnly) {
+ continue;
+ }
+
+ // always allow if the isSyncable state is unknown
+ boolean syncAllowed =
+ (isSyncable < 0)
+ || ignoreSettings
+ || (backgroundDataUsageAllowed
+ && mSyncStorageEngine.getMasterSyncAutomatically(account.userId)
+ && mSyncStorageEngine.getSyncAutomatically(account.account,
+ account.userId, authority));
+ if (!syncAllowed) {
+ if (isLoggable) {
+ Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority
+ + " is not allowed, dropping request");
+ }
+ continue;
+ }
+
+ Pair<Long, Long> backoff = mSyncStorageEngine
+ .getBackoff(account.account, account.userId, authority);
+ long delayUntil = mSyncStorageEngine.getDelayUntilTime(account.account,
+ account.userId, authority);
+ final long backoffTime = backoff != null ? backoff.first : 0;
+ if (isSyncable < 0) {
+ // Initialisation sync.
+ Bundle newExtras = new Bundle();
+ newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
+ if (isLoggable) {
+ Log.v(TAG, "schedule initialisation Sync:"
+ + ", delay until " + delayUntil
+ + ", run by " + 0
+ + ", source " + source
+ + ", account " + account
+ + ", authority " + authority
+ + ", extras " + newExtras);
+ }
+ scheduleSyncOperation(
+ new SyncOperation(account.account, account.userId, reason, source,
+ authority, newExtras, 0 /* immediate */, 0 /* No flex time*/,
+ backoffTime, delayUntil, allowParallelSyncs));
+ }
+ if (!onlyThoseWithUnkownSyncableState) {
+ if (isLoggable) {
+ Log.v(TAG, "scheduleSync:"
+ + " delay until " + delayUntil
+ + " run by " + runtimeMillis
+ + " flex " + beforeRuntimeMillis
+ + ", source " + source
+ + ", account " + account
+ + ", authority " + authority
+ + ", extras " + extras);
+ }
+ scheduleSyncOperation(
+ new SyncOperation(account.account, account.userId, reason, source,
+ authority, extras, runtimeMillis, beforeRuntimeMillis,
+ backoffTime, delayUntil, allowParallelSyncs));
}
}
}
}
+ /**
+ * Schedule sync based on local changes to a provider. Occurs within interval
+ * [LOCAL_SYNC_DELAY, 2*LOCAL_SYNC_DELAY].
+ */
public void scheduleLocalSync(Account account, int userId, int reason, String authority) {
final Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
- scheduleSync(account, userId, reason, authority, extras, LOCAL_SYNC_DELAY,
+ scheduleSync(account, userId, reason, authority, extras,
+ LOCAL_SYNC_DELAY /* earliest run time */,
+ 2 * LOCAL_SYNC_DELAY /* latest sync time. */,
false /* onlyThoseWithUnkownSyncableState */);
}
@@ -775,6 +975,7 @@
}
class SyncAlarmIntentReceiver extends BroadcastReceiver {
+ @Override
public void onReceive(Context context, Intent intent) {
mHandleAlarmWakeLock.acquire();
sendSyncAlarmMessage();
@@ -943,11 +1144,13 @@
Log.d(TAG, "retrying sync operation that failed because there was already a "
+ "sync in progress: " + operation);
}
- scheduleSyncOperation(new SyncOperation(operation.account, operation.userId,
+ scheduleSyncOperation(
+ new SyncOperation(
+ operation.account, operation.userId,
operation.reason,
operation.syncSource,
operation.authority, operation.extras,
- DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS * 1000,
+ DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS * 1000, operation.flexTime,
operation.backoff, operation.delayUntil, operation.allowParallelSyncs));
} else if (syncResult.hasSoftError()) {
if (isLoggable) {
@@ -977,7 +1180,8 @@
final Account[] accounts = AccountManagerService.getSingleton().getAccounts(userId);
for (Account account : accounts) {
scheduleSync(account, userId, SyncOperation.REASON_USER_START, null, null,
- 0 /* no delay */, true /* onlyThoseWithUnknownSyncableState */);
+ 0 /* no delay */, 0 /* No flex */,
+ true /* onlyThoseWithUnknownSyncableState */);
}
sendCheckAlarmsMessage();
@@ -1204,7 +1408,10 @@
synchronized (mSyncQueue) {
sb.setLength(0);
mSyncQueue.dump(sb);
+ // Dump Pending Operations.
+ getSyncStorageEngine().dumpPendingOperations(sb);
}
+
pw.println();
pw.print(sb.toString());
@@ -1271,12 +1478,15 @@
for (int i = 0; i < settings.periodicSyncs.size(); i++) {
- final Pair<Bundle, Long> pair = settings.periodicSyncs.get(i);
- final String period = String.valueOf(pair.second);
- final String extras = pair.first.size() > 0 ? " " + pair.first.toString() : "";
- final String next = formatTime(status.getPeriodicSyncTime(i)
- + pair.second * 1000);
- table.set(row + i * 2, 12, period + extras);
+ final PeriodicSync sync = settings.periodicSyncs.get(i);
+ final String period =
+ String.format("[p:%d s, f: %d s]", sync.period, sync.flexTime);
+ final String extras =
+ sync.extras.size() > 0 ?
+ sync.extras.toString() : "Bundle[]";
+ final String next = "Next sync: " + formatTime(status.getPeriodicSyncTime(i)
+ + sync.period * 1000);
+ table.set(row + i * 2, 12, period + " " + extras);
table.set(row + i * 2 + 1, 12, next);
}
@@ -1810,6 +2020,7 @@
super(looper);
}
+ @Override
public void handleMessage(Message msg) {
long earliestFuturePollTime = Long.MAX_VALUE;
long nextPendingSyncTime = Long.MAX_VALUE;
@@ -1827,7 +2038,7 @@
earliestFuturePollTime = scheduleReadyPeriodicSyncs();
switch (msg.what) {
case SyncHandler.MESSAGE_CANCEL: {
- Pair<Account, String> payload = (Pair<Account, String>)msg.obj;
+ Pair<Account, String> payload = (Pair<Account, String>) msg.obj;
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CANCEL: "
+ payload.first + ", " + payload.second);
@@ -1934,6 +2145,10 @@
* in milliseconds since boot
*/
private long scheduleReadyPeriodicSyncs() {
+ final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+ if (isLoggable) {
+ Log.v(TAG, "scheduleReadyPeriodicSyncs");
+ }
final boolean backgroundDataUsageAllowed =
getConnectivityManager().getBackgroundDataSetting();
long earliestFuturePollTime = Long.MAX_VALUE;
@@ -1973,37 +2188,59 @@
}
for (int i = 0, N = authorityInfo.periodicSyncs.size(); i < N; i++) {
- final Bundle extras = authorityInfo.periodicSyncs.get(i).first;
- final Long periodInMillis = authorityInfo.periodicSyncs.get(i).second * 1000;
- // Skip if the period is invalid
+ final PeriodicSync sync = authorityInfo.periodicSyncs.get(i);
+ final Bundle extras = sync.extras;
+ final Long periodInMillis = sync.period * 1000;
+ final Long flexInMillis = sync.flexTime * 1000;
+ // Skip if the period is invalid.
if (periodInMillis <= 0) {
continue;
}
- // find when this periodic sync was last scheduled to run
+ // Find when this periodic sync was last scheduled to run.
final long lastPollTimeAbsolute = status.getPeriodicSyncTime(i);
-
+ final long shiftedLastPollTimeAbsolute =
+ (0 < lastPollTimeAbsolute - mSyncRandomOffsetMillis) ?
+ (lastPollTimeAbsolute - mSyncRandomOffsetMillis) : 0;
long remainingMillis
- = periodInMillis - (shiftedNowAbsolute % periodInMillis);
-
+ = periodInMillis - (shiftedNowAbsolute % periodInMillis);
+ long timeSinceLastRunMillis
+ = (nowAbsolute - lastPollTimeAbsolute);
+ // Schedule this periodic sync to run early if it's close enough to its next
+ // runtime, and far enough from its last run time.
+ // If we are early, there will still be time remaining in this period.
+ boolean runEarly = remainingMillis <= flexInMillis
+ && timeSinceLastRunMillis > periodInMillis - flexInMillis;
+ if (isLoggable) {
+ Log.v(TAG, "sync: " + i + " for " + authorityInfo.authority + "."
+ + " period: " + (periodInMillis)
+ + " flex: " + (flexInMillis)
+ + " remaining: " + (remainingMillis)
+ + " time_since_last: " + timeSinceLastRunMillis
+ + " last poll absol: " + lastPollTimeAbsolute
+ + " last poll shifed: " + shiftedLastPollTimeAbsolute
+ + " shifted now: " + shiftedNowAbsolute
+ + " run_early: " + runEarly);
+ }
/*
* Sync scheduling strategy: Set the next periodic sync
* based on a random offset (in seconds). Also sync right
* now if any of the following cases hold and mark it as
* having been scheduled
- * Case 1: This sync is ready to run
- * now.
+ * Case 1: This sync is ready to run now.
* Case 2: If the lastPollTimeAbsolute is in the
* future, sync now and reinitialize. This can happen for
* example if the user changed the time, synced and changed
* back.
* Case 3: If we failed to sync at the last scheduled
- * time
+ * time.
+ * Case 4: This sync is close enough to the time that we can schedule it.
*/
- if (remainingMillis == periodInMillis // Case 1
+ if (runEarly // Case 4
+ || remainingMillis == periodInMillis // Case 1
|| lastPollTimeAbsolute > nowAbsolute // Case 2
- || (nowAbsolute - lastPollTimeAbsolute
- >= periodInMillis)) { // Case 3
+ || timeSinceLastRunMillis >= periodInMillis) { // Case 3
// Sync now
+
final Pair<Long, Long> backoff = mSyncStorageEngine.getBackoff(
authorityInfo.account, authorityInfo.userId,
authorityInfo.authority);
@@ -2015,24 +2252,29 @@
if (syncAdapterInfo == null) {
continue;
}
+ mSyncStorageEngine.setPeriodicSyncTime(authorityInfo.ident,
+ authorityInfo.periodicSyncs.get(i), nowAbsolute);
scheduleSyncOperation(
new SyncOperation(authorityInfo.account, authorityInfo.userId,
SyncOperation.REASON_PERIODIC,
SyncStorageEngine.SOURCE_PERIODIC,
- authorityInfo.authority, extras, 0 /* delay */,
+ authorityInfo.authority, extras,
+ 0 /* runtime */, 0 /* flex */,
backoff != null ? backoff.first : 0,
mSyncStorageEngine.getDelayUntilTime(
authorityInfo.account, authorityInfo.userId,
authorityInfo.authority),
syncAdapterInfo.type.allowParallelSyncs()));
- mSyncStorageEngine.setPeriodicSyncTime(authorityInfo.ident,
- authorityInfo.periodicSyncs.get(i), nowAbsolute);
+
}
- // Compute when this periodic sync should next run
- final long nextPollTimeAbsolute = nowAbsolute + remainingMillis;
-
- // remember this time if it is earlier than
- // earliestFuturePollTime
+ // Compute when this periodic sync should next run.
+ long nextPollTimeAbsolute;
+ if (runEarly) {
+ // Add the time remaining so we don't get out of phase.
+ nextPollTimeAbsolute = nowAbsolute + periodInMillis + remainingMillis;
+ } else {
+ nextPollTimeAbsolute = nowAbsolute + remainingMillis;
+ }
if (nextPollTimeAbsolute < earliestFuturePollTime) {
earliestFuturePollTime = nextPollTimeAbsolute;
}
@@ -2044,10 +2286,9 @@
}
// convert absolute time to elapsed time
- return SystemClock.elapsedRealtime()
- + ((earliestFuturePollTime < nowAbsolute)
- ? 0
- : (earliestFuturePollTime - nowAbsolute));
+ return SystemClock.elapsedRealtime() +
+ ((earliestFuturePollTime < nowAbsolute) ?
+ 0 : (earliestFuturePollTime - nowAbsolute));
}
private long maybeStartNextSyncLocked() {
@@ -2097,8 +2338,8 @@
Log.v(TAG, "build the operation array, syncQueue size is "
+ mSyncQueue.getOperations().size());
}
- final Iterator<SyncOperation> operationIterator = mSyncQueue.getOperations()
- .iterator();
+ final Iterator<SyncOperation> operationIterator =
+ mSyncQueue.getOperations().iterator();
final ActivityManager activityManager
= (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
@@ -2106,40 +2347,52 @@
while (operationIterator.hasNext()) {
final SyncOperation op = operationIterator.next();
- // drop the sync if the account of this operation no longer exists
+ // Drop the sync if the account of this operation no longer exists.
if (!containsAccountAndUser(accounts, op.account, op.userId)) {
operationIterator.remove();
mSyncStorageEngine.deleteFromPending(op.pendingOperation);
+ if (isLoggable) {
+ Log.v(TAG, " Dropping sync operation: account doesn't exist.");
+ }
continue;
}
- // drop this sync request if it isn't syncable
+ // Drop this sync request if it isn't syncable.
int syncableState = getIsSyncable(
op.account, op.userId, op.authority);
if (syncableState == 0) {
operationIterator.remove();
mSyncStorageEngine.deleteFromPending(op.pendingOperation);
+ if (isLoggable) {
+ Log.v(TAG, " Dropping sync operation: isSyncable == 0.");
+ }
continue;
}
- // if the user in not running, drop the request
+ // If the user is not running, drop the request.
if (!activityManager.isUserRunning(op.userId)) {
final UserInfo userInfo = mUserManager.getUserInfo(op.userId);
if (userInfo == null) {
removedUsers.add(op.userId);
}
- continue;
- }
-
- // if the next run time is in the future, meaning there are no syncs ready
- // to run, return the time
- if (op.effectiveRunTime > now) {
- if (nextReadyToRunTime > op.effectiveRunTime) {
- nextReadyToRunTime = op.effectiveRunTime;
+ if (isLoggable) {
+ Log.v(TAG, " Dropping sync operation: user not running.");
}
continue;
}
+ // If the next run time is in the future, even given the flexible scheduling,
+ // return the time.
+ if (op.effectiveRunTime - op.flexTime > now) {
+ if (nextReadyToRunTime > op.effectiveRunTime) {
+ nextReadyToRunTime = op.effectiveRunTime;
+ }
+ if (isLoggable) {
+ Log.v(TAG, " Dropping sync operation: Sync too far in future.");
+ }
+ continue;
+ }
+ // TODO: change this behaviour for non-registered syncs.
final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo;
syncAdapterInfo = mSyncAdapters.getServiceInfo(
SyncAdapterType.newKey(op.authority, op.account.type), op.userId);
@@ -2180,7 +2433,7 @@
}
// find the next operation to dispatch, if one is ready
- // iterate from the top, keep issuing (while potentially cancelling existing syncs)
+ // iterate from the top, keep issuing (while potentially canceling existing syncs)
// until the quotas are filled.
// once the quotas are filled iterate once more to find when the next one would be
// (also considering pre-emption reasons).
@@ -2460,11 +2713,13 @@
}
if (syncResult != null && syncResult.fullSyncRequested) {
- scheduleSyncOperation(new SyncOperation(syncOperation.account, syncOperation.userId,
- syncOperation.reason,
- syncOperation.syncSource, syncOperation.authority, new Bundle(), 0,
- syncOperation.backoff, syncOperation.delayUntil,
- syncOperation.allowParallelSyncs));
+ scheduleSyncOperation(
+ new SyncOperation(syncOperation.account, syncOperation.userId,
+ syncOperation.reason,
+ syncOperation.syncSource, syncOperation.authority, new Bundle(),
+ 0 /* delay */, 0 /* flex */,
+ syncOperation.backoff, syncOperation.delayUntil,
+ syncOperation.allowParallelSyncs));
}
// no need to schedule an alarm, as that will be done by our caller.
}
@@ -2637,6 +2892,8 @@
final boolean alarmIsActive = mAlarmScheduleTime != null;
final boolean needAlarm = alarmTime != Long.MAX_VALUE;
if (needAlarm) {
+ // Need the alarm if it's currently not set, or if our time is before the currently
+ // set time.
if (!alarmIsActive || alarmTime < mAlarmScheduleTime) {
shouldSet = true;
}
@@ -2644,7 +2901,7 @@
shouldCancel = alarmIsActive;
}
- // set or cancel the alarm as directed
+ // Set or cancel the alarm as directed.
ensureAlarmService();
if (shouldSet) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
diff --git a/services/java/com/android/server/content/SyncOperation.java b/services/java/com/android/server/content/SyncOperation.java
index eaad982..b688535 100644
--- a/services/java/com/android/server/content/SyncOperation.java
+++ b/services/java/com/android/server/content/SyncOperation.java
@@ -18,13 +18,18 @@
import android.accounts.Account;
import android.content.pm.PackageManager;
+import android.content.ComponentName;
import android.content.ContentResolver;
+import android.content.SyncRequest;
import android.os.Bundle;
import android.os.SystemClock;
+import android.util.Pair;
/**
* Value type that represents a sync operation.
- * @hide
+ * TODO: This is the class to flesh out with all the scheduling data - metered/unmetered,
+ * transfer-size, etc.
+ * {@hide}
*/
public class SyncOperation implements Comparable {
public static final int REASON_BACKGROUND_DATA_SETTINGS_CHANGED = -1;
@@ -32,7 +37,9 @@
public static final int REASON_SERVICE_CHANGED = -3;
public static final int REASON_PERIODIC = -4;
public static final int REASON_IS_SYNCABLE = -5;
+ /** Sync started because it has just been set to sync automatically. */
public static final int REASON_SYNC_AUTO = -6;
+ /** Sync started because master sync automatically has been set to true. */
public static final int REASON_MASTER_SYNC_AUTO = -7;
public static final int REASON_USER_START = -8;
@@ -47,75 +54,143 @@
"UserStart",
};
+ /** Account info to identify a SyncAdapter registered with the system. */
public final Account account;
+ /** Authority info to identify a SyncAdapter registered with the system. */
+ public final String authority;
+ /** Service to which this operation will bind to perform the sync. */
+ public final ComponentName service;
public final int userId;
public final int reason;
public int syncSource;
- public String authority;
public final boolean allowParallelSyncs;
public Bundle extras;
public final String key;
- public long earliestRunTime;
public boolean expedited;
public SyncStorageEngine.PendingOperation pendingOperation;
+ /** Elapsed real time in millis at which to run this sync. */
+ public long latestRunTime;
+ /** Set by the SyncManager in order to delay retries. */
public Long backoff;
+ /** Specified by the adapter to delay subsequent sync operations. */
public long delayUntil;
+ /**
+ * Elapsed real time in millis when this sync will be run.
+ * Depends on max(backoff, latestRunTime, and delayUntil).
+ */
public long effectiveRunTime;
+ /** Amount of time before {@link effectiveRunTime} from which this sync can run. */
+ public long flexTime;
public SyncOperation(Account account, int userId, int reason, int source, String authority,
- Bundle extras, long delayInMs, long backoff, long delayUntil,
- boolean allowParallelSyncs) {
+ Bundle extras, long runTimeFromNow, long flexTime, long backoff,
+ long delayUntil, boolean allowParallelSyncs) {
+ this.service = null;
this.account = account;
+ this.authority = authority;
this.userId = userId;
this.reason = reason;
this.syncSource = source;
- this.authority = authority;
this.allowParallelSyncs = allowParallelSyncs;
this.extras = new Bundle(extras);
- removeFalseExtra(ContentResolver.SYNC_EXTRAS_UPLOAD);
- removeFalseExtra(ContentResolver.SYNC_EXTRAS_MANUAL);
- removeFalseExtra(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS);
- removeFalseExtra(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF);
- removeFalseExtra(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY);
- removeFalseExtra(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS);
- removeFalseExtra(ContentResolver.SYNC_EXTRAS_EXPEDITED);
- removeFalseExtra(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS);
+ cleanBundle(this.extras);
this.delayUntil = delayUntil;
this.backoff = backoff;
final long now = SystemClock.elapsedRealtime();
- if (delayInMs < 0) {
+ // Check the extras bundle. Must occur after we set the internal bundle.
+ if (runTimeFromNow < 0 || isExpedited()) {
this.expedited = true;
- this.earliestRunTime = now;
+ this.latestRunTime = now;
+ this.flexTime = 0;
} else {
this.expedited = false;
- this.earliestRunTime = now + delayInMs;
+ this.latestRunTime = now + runTimeFromNow;
+ this.flexTime = flexTime;
}
updateEffectiveRunTime();
this.key = toKey();
}
- private void removeFalseExtra(String extraName) {
- if (!extras.getBoolean(extraName, false)) {
- extras.remove(extraName);
+ public SyncOperation(SyncRequest request, int userId, int reason, int source, long backoff,
+ long delayUntil, boolean allowParallelSyncs) {
+ if (request.hasAuthority()) {
+ Pair<Account, String> providerInfo = request.getProviderInfo();
+ this.account = providerInfo.first;
+ this.authority = providerInfo.second;
+ this.service = null;
+ } else {
+ this.service = request.getService();
+ this.account = null;
+ this.authority = null;
+ }
+ this.userId = userId;
+ this.reason = reason;
+ this.syncSource = source;
+ this.allowParallelSyncs = allowParallelSyncs;
+ this.extras = new Bundle(extras);
+ cleanBundle(this.extras);
+ this.delayUntil = delayUntil;
+ this.backoff = backoff;
+ final long now = SystemClock.elapsedRealtime();
+ if (request.isExpedited()) {
+ this.expedited = true;
+ this.latestRunTime = now;
+ this.flexTime = 0;
+ } else {
+ this.expedited = false;
+ this.latestRunTime = now + (request.getSyncRunTime() * 1000);
+ this.flexTime = request.getSyncFlexTime() * 1000;
+ }
+ updateEffectiveRunTime();
+ this.key = toKey();
+ }
+
+ /**
+ * Make sure the bundle attached to this SyncOperation doesn't have unnecessary
+ * flags set.
+ * @param bundle to clean.
+ */
+ private void cleanBundle(Bundle bundle) {
+ removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_UPLOAD);
+ removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_MANUAL);
+ removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS);
+ removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF);
+ removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY);
+ removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS);
+ removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_EXPEDITED);
+ removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS);
+ removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_ALLOW_METERED);
+
+ // Remove Config data.
+ bundle.remove(ContentResolver.SYNC_EXTRAS_EXPECTED_UPLOAD);
+ bundle.remove(ContentResolver.SYNC_EXTRAS_EXPECTED_DOWNLOAD);
+ }
+
+ private void removeFalseExtra(Bundle bundle, String extraName) {
+ if (!bundle.getBoolean(extraName, false)) {
+ bundle.remove(extraName);
}
}
+ /** Only used to immediately reschedule a sync. */
SyncOperation(SyncOperation other) {
+ this.service = other.service;
this.account = other.account;
+ this.authority = other.authority;
this.userId = other.userId;
this.reason = other.reason;
this.syncSource = other.syncSource;
- this.authority = other.authority;
this.extras = new Bundle(other.extras);
this.expedited = other.expedited;
- this.earliestRunTime = SystemClock.elapsedRealtime();
+ this.latestRunTime = SystemClock.elapsedRealtime();
+ this.flexTime = 0L;
this.backoff = other.backoff;
- this.delayUntil = other.delayUntil;
this.allowParallelSyncs = other.allowParallelSyncs;
this.updateEffectiveRunTime();
this.key = toKey();
}
+ @Override
public String toString() {
return dump(null, true);
}
@@ -131,8 +206,8 @@
.append(authority)
.append(", ")
.append(SyncStorageEngine.SOURCES[syncSource])
- .append(", earliestRunTime ")
- .append(earliestRunTime);
+ .append(", latestRunTime ")
+ .append(latestRunTime);
if (expedited) {
sb.append(", EXPEDITED");
}
@@ -170,23 +245,38 @@
}
}
+ public boolean isMetered() {
+ return extras.getBoolean(ContentResolver.SYNC_EXTRAS_ALLOW_METERED, false);
+ }
+
public boolean isInitialization() {
return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
}
public boolean isExpedited() {
- return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
+ return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false) || expedited;
}
public boolean ignoreBackoff() {
return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
}
+ /** Changed in V3. */
private String toKey() {
StringBuilder sb = new StringBuilder();
- sb.append("authority: ").append(authority);
- sb.append(" account {name=" + account.name + ", user=" + userId + ", type=" + account.type
- + "}");
+ if (service == null) {
+ sb.append("authority: ").append(authority);
+ sb.append(" account {name=" + account.name + ", user=" + userId + ", type=" + account.type
+ + "}");
+ } else {
+ sb.append("service {package=" )
+ .append(service.getPackageName())
+ .append(" user=")
+ .append(userId)
+ .append(", class=")
+ .append(service.getClassName())
+ .append("}");
+ }
sb.append(" extras: ");
extrasToStringBuilder(extras, sb);
return sb.toString();
@@ -200,25 +290,40 @@
sb.append("]");
}
+ /**
+ * Update the effective run time of this Operation based on latestRunTime (specified at
+ * creation time of sync), delayUntil (specified by SyncAdapter), or backoff (specified by
+ * SyncManager on soft failures).
+ */
public void updateEffectiveRunTime() {
- effectiveRunTime = ignoreBackoff()
- ? earliestRunTime
- : Math.max(
- Math.max(earliestRunTime, delayUntil),
- backoff);
+ // Regardless of whether we're in backoff or honouring a delayUntil, we still incorporate
+ // the flex time provided by the developer.
+ effectiveRunTime = ignoreBackoff() ?
+ latestRunTime :
+ Math.max(Math.max(latestRunTime, delayUntil), backoff);
}
+ /**
+ * If two SyncOperation intervals are disjoint, the smaller is the interval that occurs before.
+ * If the intervals overlap, the two are considered equal.
+ */
+ @Override
public int compareTo(Object o) {
- SyncOperation other = (SyncOperation)o;
-
+ SyncOperation other = (SyncOperation) o;
if (expedited != other.expedited) {
return expedited ? -1 : 1;
}
-
- if (effectiveRunTime == other.effectiveRunTime) {
+ long x1 = effectiveRunTime - flexTime;
+ long y1 = effectiveRunTime;
+ long x2 = other.effectiveRunTime - other.flexTime;
+ long y2 = other.effectiveRunTime;
+ // Overlapping intervals.
+ if ((x1 <= y2 && x1 >= x2) || (x2 <= y1 && x2 >= x1)) {
return 0;
}
-
- return effectiveRunTime < other.effectiveRunTime ? -1 : 1;
+ if (x1 < x2 && y1 < x2) {
+ return -1;
+ }
+ return 1;
}
}
diff --git a/services/java/com/android/server/content/SyncQueue.java b/services/java/com/android/server/content/SyncQueue.java
index 951e92c..6f3fe6e 100644
--- a/services/java/com/android/server/content/SyncQueue.java
+++ b/services/java/com/android/server/content/SyncQueue.java
@@ -73,7 +73,7 @@
}
SyncOperation syncOperation = new SyncOperation(
op.account, op.userId, op.reason, op.syncSource, op.authority, op.extras,
- 0 /* delay */, backoff != null ? backoff.first : 0,
+ 0 /* delay */, 0 /* flex */, backoff != null ? backoff.first : 0,
mSyncStorageEngine.getDelayUntilTime(op.account, op.userId, op.authority),
syncAdapterInfo.type.allowParallelSyncs());
syncOperation.expedited = op.expedited;
@@ -86,35 +86,40 @@
return add(operation, null /* this is not coming from the database */);
}
+ /**
+ * Adds a SyncOperation to the queue and creates a PendingOperation object to track that sync.
+ * If an operation is added that already exists, the existing operation is updated if the newly
+ * added operation occurs before (or the interval overlaps).
+ */
private boolean add(SyncOperation operation,
SyncStorageEngine.PendingOperation pop) {
- // - if an operation with the same key exists and this one should run earlier,
- // update the earliestRunTime of the existing to the new time
- // - if an operation with the same key exists and if this one should run
- // later, ignore it
- // - if no operation exists then add the new one
+ // If an operation with the same key exists and this one should run sooner/overlaps,
+ // replace the run interval of the existing operation with this new one.
+ // Complications: what if the existing operation is expedited but the new operation has an
+ // earlier run time? Will not be a problem for periodic syncs (no expedited flag), and for
+ // one-off syncs we only change it if the new sync is sooner.
final String operationKey = operation.key;
final SyncOperation existingOperation = mOperationsMap.get(operationKey);
if (existingOperation != null) {
boolean changed = false;
- if (existingOperation.expedited == operation.expedited) {
- final long newRunTime =
- Math.min(existingOperation.earliestRunTime, operation.earliestRunTime);
- if (existingOperation.earliestRunTime != newRunTime) {
- existingOperation.earliestRunTime = newRunTime;
- changed = true;
- }
- } else {
- if (operation.expedited) {
- existingOperation.expedited = true;
- changed = true;
- }
+ if (operation.compareTo(existingOperation) <= 0 ) {
+ existingOperation.expedited = operation.expedited;
+ long newRunTime =
+ Math.min(existingOperation.latestRunTime, operation.latestRunTime);
+ // Take smaller runtime.
+ existingOperation.latestRunTime = newRunTime;
+ // Take newer flextime.
+ existingOperation.flexTime = operation.flexTime;
+ changed = true;
}
return changed;
}
operation.pendingOperation = pop;
+ // Don't update the PendingOp if one already exists. This really is just a placeholder,
+ // no actual scheduling info is placed here.
+ // TODO: Change this to support service components.
if (operation.pendingOperation == null) {
pop = new SyncStorageEngine.PendingOperation(
operation.account, operation.userId, operation.reason, operation.syncSource,
diff --git a/services/java/com/android/server/content/SyncStorageEngine.java b/services/java/com/android/server/content/SyncStorageEngine.java
index c4dc575..0b99fca 100644
--- a/services/java/com/android/server/content/SyncStorageEngine.java
+++ b/services/java/com/android/server/content/SyncStorageEngine.java
@@ -18,6 +18,7 @@
import android.accounts.Account;
import android.accounts.AccountAndUser;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ISyncStatusObserver;
@@ -44,7 +45,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastXmlSerializer;
-import com.android.server.content.SyncStorageEngine.AuthorityInfo;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -70,8 +70,8 @@
public class SyncStorageEngine extends Handler {
private static final String TAG = "SyncManager";
- private static final boolean DEBUG = false;
- private static final boolean DEBUG_FILE = false;
+ private static final boolean DEBUG = true;
+ private static final boolean DEBUG_FILE = true;
private static final String XML_ATTR_NEXT_AUTHORITY_ID = "nextAuthorityId";
private static final String XML_ATTR_LISTEN_FOR_TICKLES = "listen-for-tickles";
@@ -80,8 +80,15 @@
private static final String XML_ATTR_USER = "user";
private static final String XML_TAG_LISTEN_FOR_TICKLES = "listenForTickles";
+ /** Default time for a periodic sync. */
private static final long DEFAULT_POLL_FREQUENCY_SECONDS = 60 * 60 * 24; // One day
+ /** Percentage of period that is flex by default, if no flex is set. */
+ private static final double DEFAULT_FLEX_PERCENT_SYNC = 0.04;
+
+ /** Lower bound on sync time from which we assign a default flex time. */
+ private static final long DEFAULT_MIN_FLEX_ALLOWED_SECS = 5;
+
@VisibleForTesting
static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
@@ -154,12 +161,13 @@
final int syncSource;
final String authority;
final Bundle extras; // note: read-only.
+ final ComponentName serviceName;
final boolean expedited;
int authorityId;
byte[] flatExtras;
- PendingOperation(Account account, int userId, int reason,int source,
+ PendingOperation(Account account, int userId, int reason, int source,
String authority, Bundle extras, boolean expedited) {
this.account = account;
this.userId = userId;
@@ -169,6 +177,7 @@
this.extras = extras != null ? new Bundle(extras) : extras;
this.expedited = expedited;
this.authorityId = -1;
+ this.serviceName = null;
}
PendingOperation(PendingOperation other) {
@@ -180,6 +189,7 @@
this.extras = other.extras;
this.authorityId = other.authorityId;
this.expedited = other.expedited;
+ this.serviceName = other.serviceName;
}
}
@@ -194,6 +204,7 @@
}
public static class AuthorityInfo {
+ final ComponentName service;
final Account account;
final int userId;
final String authority;
@@ -203,7 +214,7 @@
long backoffTime;
long backoffDelay;
long delayUntil;
- final ArrayList<Pair<Bundle, Long>> periodicSyncs;
+ final ArrayList<PeriodicSync> periodicSyncs;
/**
* Copy constructor for making deep-ish copies. Only the bundles stored
@@ -215,30 +226,70 @@
account = toCopy.account;
userId = toCopy.userId;
authority = toCopy.authority;
+ service = toCopy.service;
ident = toCopy.ident;
enabled = toCopy.enabled;
syncable = toCopy.syncable;
backoffTime = toCopy.backoffTime;
backoffDelay = toCopy.backoffDelay;
delayUntil = toCopy.delayUntil;
- periodicSyncs = new ArrayList<Pair<Bundle, Long>>();
- for (Pair<Bundle, Long> sync : toCopy.periodicSyncs) {
+ periodicSyncs = new ArrayList<PeriodicSync>();
+ for (PeriodicSync sync : toCopy.periodicSyncs) {
// Still not a perfect copy, because we are just copying the mappings.
- periodicSyncs.add(Pair.create(new Bundle(sync.first), sync.second));
+ periodicSyncs.add(new PeriodicSync(sync));
}
}
+ /**
+ * Create an authority with one periodic sync scheduled with an empty bundle and syncing
+ * every day. An empty bundle is considered equal to any other bundle see
+ * {@link PeriodicSync.syncExtrasEquals}.
+ * @param account Account that this authority syncs.
+ * @param userId which user this sync is registered for.
+ * @param userId user for which this authority is registered.
+ * @param ident id of this authority.
+ */
AuthorityInfo(Account account, int userId, String authority, int ident) {
this.account = account;
this.userId = userId;
this.authority = authority;
+ this.service = null;
this.ident = ident;
enabled = SYNC_ENABLED_DEFAULT;
syncable = -1; // default to "unknown"
backoffTime = -1; // if < 0 then we aren't in backoff mode
backoffDelay = -1; // if < 0 then we aren't in backoff mode
- periodicSyncs = new ArrayList<Pair<Bundle, Long>>();
- periodicSyncs.add(Pair.create(new Bundle(), DEFAULT_POLL_FREQUENCY_SECONDS));
+ periodicSyncs = new ArrayList<PeriodicSync>();
+ // Old version adds one periodic sync a day.
+ periodicSyncs.add(new PeriodicSync(account, authority,
+ new Bundle(),
+ DEFAULT_POLL_FREQUENCY_SECONDS,
+ calculateDefaultFlexTime(DEFAULT_POLL_FREQUENCY_SECONDS)));
+ }
+
+ /**
+ * Create an authority with one periodic sync scheduled with an empty bundle and syncing
+ * every day using a sync service.
+ * @param cname sync service identifier.
+ * @param userId user for which this authority is registered.
+ * @param ident id of this authority.
+ */
+ AuthorityInfo(ComponentName cname, int userId, int ident) {
+ this.account = null;
+ this.userId = userId;
+ this.authority = null;
+ this.service = cname;
+ this.ident = ident;
+ // Sync service is always enabled.
+ enabled = true;
+ syncable = -1; // default to "unknown"
+ backoffTime = -1; // if < 0 then we aren't in backoff mode
+ backoffDelay = -1; // if < 0 then we aren't in backoff mode
+ periodicSyncs = new ArrayList<PeriodicSync>();
+ periodicSyncs.add(new PeriodicSync(account, authority,
+ new Bundle(),
+ DEFAULT_POLL_FREQUENCY_SECONDS,
+ calculateDefaultFlexTime(DEFAULT_POLL_FREQUENCY_SECONDS)));
}
}
@@ -304,6 +355,10 @@
private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners
= new RemoteCallbackList<ISyncStatusObserver>();
+ /** Reverse mapping for component name -> <userid -> authority id>. */
+ private final HashMap<ComponentName, SparseArray<AuthorityInfo>> mServices =
+ new HashMap<ComponentName, SparseArray<AuthorityInfo>>();
+
private int mNextAuthorityId = 0;
// We keep 4 weeks of stats.
@@ -436,6 +491,28 @@
}
}
+ /**
+ * Figure out a reasonable flex time for cases where none is provided (old api calls).
+ * @param syncTimeSeconds requested sync time from now.
+ * @return amount of seconds before syncTimeSeconds that the sync can occur.
+ * I.e.
+ * earliest_sync_time = syncTimeSeconds - calculateDefaultFlexTime(syncTimeSeconds)
+ * The flex time is capped at a percentage of the {@link DEFAULT_POLL_FREQUENCY_SECONDS}.
+ */
+ public static long calculateDefaultFlexTime(long syncTimeSeconds) {
+ if (syncTimeSeconds < DEFAULT_MIN_FLEX_ALLOWED_SECS) {
+ // Small enough sync request time that we don't add flex time - developer probably
+ // wants to wait for an operation to occur before syncing so we honour the
+ // request time.
+ return 0L;
+ } else if (syncTimeSeconds < DEFAULT_POLL_FREQUENCY_SECONDS) {
+ return (long) (syncTimeSeconds * DEFAULT_FLEX_PERCENT_SYNC);
+ } else {
+ // Large enough sync request time that we cap the flex time.
+ return (long) (DEFAULT_POLL_FREQUENCY_SECONDS * DEFAULT_FLEX_PERCENT_SYNC);
+ }
+ }
+
private void reportChange(int which) {
ArrayList<ISyncStatusObserver> reports = null;
synchronized (mAuthorities) {
@@ -553,8 +630,8 @@
+ ", user " + userId + " -> " + syncable);
}
synchronized (mAuthorities) {
- AuthorityInfo authority = getOrCreateAuthorityLocked(account, userId, providerName, -1,
- false);
+ AuthorityInfo authority =
+ getOrCreateAuthorityLocked(account, userId, providerName, -1, false);
if (authority.syncable == syncable) {
if (DEBUG) {
Log.d(TAG, "setIsSyncable: already set to " + syncable + ", doing nothing");
@@ -689,62 +766,65 @@
}
}
- private void updateOrRemovePeriodicSync(Account account, int userId, String providerName,
- Bundle extras,
- long period, boolean add) {
- if (period <= 0) {
- period = 0;
- }
- if (extras == null) {
- extras = new Bundle();
- }
+ private void updateOrRemovePeriodicSync(PeriodicSync toUpdate, int userId, boolean add) {
if (DEBUG) {
- Log.v(TAG, "addOrRemovePeriodicSync: " + account + ", user " + userId
- + ", provider " + providerName
- + " -> period " + period + ", extras " + extras);
+ Log.v(TAG, "addOrRemovePeriodicSync: " + toUpdate.account + ", user " + userId
+ + ", provider " + toUpdate.authority
+ + " -> period " + toUpdate.period + ", extras " + toUpdate.extras);
}
synchronized (mAuthorities) {
+ if (toUpdate.period <= 0 && add) {
+ Log.e(TAG, "period < 0, should never happen in updateOrRemovePeriodicSync: add-" + add);
+ }
+ if (toUpdate.extras == null) {
+ Log.e(TAG, "period < 0, should never happen in updateOrRemovePeriodicSync: add-" + add);
+ }
try {
AuthorityInfo authority =
- getOrCreateAuthorityLocked(account, userId, providerName, -1, false);
+ getOrCreateAuthorityLocked(toUpdate.account, userId, toUpdate.authority,
+ -1, false);
if (add) {
- // add this periodic sync if one with the same extras doesn't already
- // exist in the periodicSyncs array
+ // add this periodic sync if an equivalent periodic doesn't already exist.
boolean alreadyPresent = false;
for (int i = 0, N = authority.periodicSyncs.size(); i < N; i++) {
- Pair<Bundle, Long> syncInfo = authority.periodicSyncs.get(i);
- final Bundle existingExtras = syncInfo.first;
- if (PeriodicSync.syncExtrasEquals(existingExtras, extras)) {
- if (syncInfo.second == period) {
+ PeriodicSync syncInfo = authority.periodicSyncs.get(i);
+ if (PeriodicSync.syncExtrasEquals(
+ toUpdate.extras,
+ syncInfo.extras)) {
+ if (toUpdate.period == syncInfo.period &&
+ toUpdate.flexTime == syncInfo.flexTime) {
+ // Absolutely the same.
return;
}
- authority.periodicSyncs.set(i, Pair.create(extras, period));
+ authority.periodicSyncs.set(i, new PeriodicSync(toUpdate));
alreadyPresent = true;
break;
}
}
- // if we added an entry to the periodicSyncs array also add an entry to
- // the periodic syncs status to correspond to it
+ // If we added an entry to the periodicSyncs array also add an entry to
+ // the periodic syncs status to correspond to it.
if (!alreadyPresent) {
- authority.periodicSyncs.add(Pair.create(extras, period));
+ authority.periodicSyncs.add(new PeriodicSync(toUpdate));
SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
status.setPeriodicSyncTime(authority.periodicSyncs.size() - 1, 0);
}
} else {
- // remove any periodic syncs that match the authority and extras
+ // Remove any periodic syncs that match the authority and extras.
SyncStatusInfo status = mSyncStatus.get(authority.ident);
boolean changed = false;
- Iterator<Pair<Bundle, Long>> iterator = authority.periodicSyncs.iterator();
+ Iterator<PeriodicSync> iterator = authority.periodicSyncs.iterator();
int i = 0;
while (iterator.hasNext()) {
- Pair<Bundle, Long> syncInfo = iterator.next();
- if (PeriodicSync.syncExtrasEquals(syncInfo.first, extras)) {
+ PeriodicSync syncInfo = iterator.next();
+ if (PeriodicSync.syncExtrasEquals(syncInfo.extras, toUpdate.extras)) {
iterator.remove();
changed = true;
- // if we removed an entry from the periodicSyncs array also
+ // If we removed an entry from the periodicSyncs array also
// remove the corresponding entry from the status
if (status != null) {
status.removePeriodicSyncTime(i);
+ } else {
+ Log.e(TAG, "Tried removing sync status on remove periodic sync but did not find it.");
}
} else {
i++;
@@ -763,16 +843,12 @@
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
}
- public void addPeriodicSync(Account account, int userId, String providerName, Bundle extras,
- long pollFrequency) {
- updateOrRemovePeriodicSync(account, userId, providerName, extras, pollFrequency,
- true /* add */);
+ public void addPeriodicSync(PeriodicSync toAdd, int userId) {
+ updateOrRemovePeriodicSync(toAdd, userId, true /* add */);
}
- public void removePeriodicSync(Account account, int userId, String providerName,
- Bundle extras) {
- updateOrRemovePeriodicSync(account, userId, providerName, extras, 0 /* period, ignored */,
- false /* remove */);
+ public void removePeriodicSync(PeriodicSync toRemove, int userId) {
+ updateOrRemovePeriodicSync(toRemove, userId, false /* remove */);
}
public List<PeriodicSync> getPeriodicSyncs(Account account, int userId, String providerName) {
@@ -781,9 +857,9 @@
AuthorityInfo authority = getAuthorityLocked(account, userId, providerName,
"getPeriodicSyncs");
if (authority != null) {
- for (Pair<Bundle, Long> item : authority.periodicSyncs) {
- syncs.add(new PeriodicSync(account, providerName, item.first,
- item.second));
+ for (PeriodicSync item : authority.periodicSyncs) {
+ // Copy and send out. Necessary for thread-safety although it's parceled.
+ syncs.add(new PeriodicSync(item));
}
}
}
@@ -866,7 +942,7 @@
op = new PendingOperation(op);
op.authorityId = authority.ident;
mPendingOperations.add(op);
- appendPendingOperationLocked(op);
+ writePendingOperationsLocked();
SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
status.pending = true;
@@ -876,6 +952,14 @@
return op;
}
+ /**
+ * Remove from list of pending operations. If successful, search through list for matching
+ * authorities. If there are no more pending syncs for the same authority/account/userid,
+ * update the SyncStatusInfo for that authority(authority here is the internal representation
+ * of a 'sync operation'.
+ * @param op
+ * @return
+ */
public boolean deleteFromPending(PendingOperation op) {
boolean res = false;
synchronized (mAuthorities) {
@@ -898,7 +982,7 @@
AuthorityInfo authority = getAuthorityLocked(op.account, op.userId, op.authority,
"deleteFromPending");
if (authority != null) {
- if (DEBUG) Log.v(TAG, "removing - " + authority);
+ if (DEBUG) Log.v(TAG, "removing - " + authority.toString());
final int N = mPendingOperations.size();
boolean morePending = false;
for (int i=0; i<N; i++) {
@@ -1391,6 +1475,65 @@
return authority;
}
+ /**
+ * Retrieve an authority, returning null if one does not exist.
+ *
+ * @param service The service name used for this sync.
+ * @param userId The user for whom this sync is scheduled.
+ * @param tag If non-null, this will be used in a log message if the
+ * requested authority does not exist.
+ */
+ private AuthorityInfo getAuthorityLocked(ComponentName service, int userId, String tag) {
+ AuthorityInfo authority = mServices.get(service).get(userId);
+ if (authority == null) {
+ if (tag != null) {
+ if (DEBUG) {
+ Log.v(TAG, tag + " No authority info found for " + service + " for user "
+ + userId);
+ }
+ }
+ return null;
+ }
+ return authority;
+ }
+
+ /**
+ * @param cname identifier for the service.
+ * @param userId for the syncs corresponding to this authority.
+ * @param ident unique identifier for authority. -1 for none.
+ * @param doWrite if true, update the accounts.xml file on the disk.
+ * @return the authority that corresponds to the provided sync service, creating it if none
+ * exists.
+ */
+ private AuthorityInfo getOrCreateAuthorityLocked(ComponentName cname, int userId, int ident,
+ boolean doWrite) {
+ SparseArray<AuthorityInfo> aInfo = mServices.get(cname);
+ if (aInfo == null) {
+ aInfo = new SparseArray<AuthorityInfo>();
+ mServices.put(cname, aInfo);
+ }
+ AuthorityInfo authority = aInfo.get(userId);
+ if (authority == null) {
+ if (ident < 0) {
+ ident = mNextAuthorityId;
+ mNextAuthorityId++;
+ doWrite = true;
+ }
+ if (DEBUG) {
+ Log.v(TAG, "created a new AuthorityInfo for " + cname.getPackageName()
+ + ", " + cname.getClassName()
+ + ", user: " + userId);
+ }
+ authority = new AuthorityInfo(cname, userId, ident);
+ aInfo.put(userId, authority);
+ mAuthorities.put(ident, authority);
+ if (doWrite) {
+ writeAccountInfoLocked();
+ }
+ }
+ return authority;
+ }
+
private AuthorityInfo getOrCreateAuthorityLocked(Account accountName, int userId,
String authorityName, int ident, boolean doWrite) {
AccountAndUser au = new AccountAndUser(accountName, userId);
@@ -1441,22 +1584,20 @@
* authority id and target periodic sync
*/
public void setPeriodicSyncTime(
- int authorityId, Pair<Bundle, Long> targetPeriodicSync, long when) {
+ int authorityId, PeriodicSync targetPeriodicSync, long when) {
boolean found = false;
final AuthorityInfo authorityInfo;
synchronized (mAuthorities) {
authorityInfo = mAuthorities.get(authorityId);
for (int i = 0; i < authorityInfo.periodicSyncs.size(); i++) {
- Pair<Bundle, Long> periodicSync = authorityInfo.periodicSyncs.get(i);
- if (PeriodicSync.syncExtrasEquals(periodicSync.first, targetPeriodicSync.first)
- && periodicSync.second == targetPeriodicSync.second) {
+ PeriodicSync periodicSync = authorityInfo.periodicSyncs.get(i);
+ if (targetPeriodicSync.equals(periodicSync)) {
mSyncStatus.get(authorityId).setPeriodicSyncTime(i, when);
found = true;
break;
}
}
}
-
if (!found) {
Log.w(TAG, "Ignoring setPeriodicSyncTime request for a sync that does not exist. " +
"Authority: " + authorityInfo.authority);
@@ -1494,6 +1635,7 @@
synchronized (mAuthorities) {
mAuthorities.clear();
mAccounts.clear();
+ mServices.clear();
mPendingOperations.clear();
mSyncStatus.clear();
mSyncHistory.clear();
@@ -1555,7 +1697,7 @@
mMasterSyncAutomatically.put(0, listen == null || Boolean.parseBoolean(listen));
eventType = parser.next();
AuthorityInfo authority = null;
- Pair<Bundle, Long> periodicSync = null;
+ PeriodicSync periodicSync = null;
do {
if (eventType == XmlPullParser.START_TAG) {
tagName = parser.getName();
@@ -1575,7 +1717,7 @@
}
} else if (parser.getDepth() == 4 && periodicSync != null) {
if ("extra".equals(tagName)) {
- parseExtra(parser, periodicSync);
+ parseExtra(parser, periodicSync.extras);
}
}
}
@@ -1669,8 +1811,7 @@
AuthorityInfo authority = null;
int id = -1;
try {
- id = Integer.parseInt(parser.getAttributeValue(
- null, "id"));
+ id = Integer.parseInt(parser.getAttributeValue(null, "id"));
} catch (NumberFormatException e) {
Log.e(TAG, "error parsing the id of the authority", e);
} catch (NullPointerException e) {
@@ -1683,6 +1824,8 @@
String accountName = parser.getAttributeValue(null, "account");
String accountType = parser.getAttributeValue(null, "type");
String user = parser.getAttributeValue(null, XML_ATTR_USER);
+ String packageName = parser.getAttributeValue(null, "package");
+ String className = parser.getAttributeValue(null, "class");
int userId = user == null ? 0 : Integer.parseInt(user);
if (accountType == null) {
accountType = "com.google";
@@ -1695,12 +1838,19 @@
+ " enabled=" + enabled
+ " syncable=" + syncable);
if (authority == null) {
- if (DEBUG_FILE) Log.v(TAG, "Creating entry");
- authority = getOrCreateAuthorityLocked(
- new Account(accountName, accountType), userId, authorityName, id, false);
+ if (DEBUG_FILE) {
+ Log.v(TAG, "Creating entry");
+ }
+ if (accountName != null && accountType != null) {
+ authority = getOrCreateAuthorityLocked(
+ new Account(accountName, accountType), userId, authorityName, id, false);
+ } else {
+ authority = getOrCreateAuthorityLocked(
+ new ComponentName(packageName, className), userId, id, false);
+ }
// If the version is 0 then we are upgrading from a file format that did not
// know about periodic syncs. In that case don't clear the list since we
- // want the default, which is a daily periodioc sync.
+ // want the default, which is a daily periodic sync.
// Otherwise clear out this default list since we will populate it later with
// the periodic sync descriptions that are read from the configuration file.
if (version > 0) {
@@ -1722,14 +1872,18 @@
+ " syncable=" + syncable);
}
}
-
return authority;
}
- private Pair<Bundle, Long> parsePeriodicSync(XmlPullParser parser, AuthorityInfo authority) {
- Bundle extras = new Bundle();
+ /**
+ * Parse a periodic sync from accounts.xml. Sets the bundle to be empty.
+ */
+ private PeriodicSync parsePeriodicSync(XmlPullParser parser, AuthorityInfo authority) {
+ Bundle extras = new Bundle(); // Gets filled in later.
String periodValue = parser.getAttributeValue(null, "period");
+ String flexValue = parser.getAttributeValue(null, "flex");
final long period;
+ long flextime;
try {
period = Long.parseLong(periodValue);
} catch (NumberFormatException e) {
@@ -1739,14 +1893,24 @@
Log.e(TAG, "the period of a periodic sync is null", e);
return null;
}
- final Pair<Bundle, Long> periodicSync = Pair.create(extras, period);
+ try {
+ flextime = Long.parseLong(flexValue);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Error formatting value parsed for periodic sync flex: " + flexValue);
+ flextime = calculateDefaultFlexTime(period);
+ } catch (NullPointerException expected) {
+ flextime = calculateDefaultFlexTime(period);
+ Log.d(TAG, "No flex time specified for this sync, using a default. period: "
+ + period + " flex: " + flextime);
+ }
+ final PeriodicSync periodicSync =
+ new PeriodicSync(authority.account, authority.authority, extras,
+ period, flextime);
authority.periodicSyncs.add(periodicSync);
-
return periodicSync;
}
- private void parseExtra(XmlPullParser parser, Pair<Bundle, Long> periodicSync) {
- final Bundle extras = periodicSync.first;
+ private void parseExtra(XmlPullParser parser, Bundle extras) {
String name = parser.getAttributeValue(null, "name");
String type = parser.getAttributeValue(null, "type");
String value1 = parser.getAttributeValue(null, "value1");
@@ -1806,62 +1970,37 @@
}
final int N = mAuthorities.size();
- for (int i=0; i<N; i++) {
+ for (int i = 0; i < N; i++) {
AuthorityInfo authority = mAuthorities.valueAt(i);
out.startTag(null, "authority");
out.attribute(null, "id", Integer.toString(authority.ident));
- out.attribute(null, "account", authority.account.name);
out.attribute(null, XML_ATTR_USER, Integer.toString(authority.userId));
- out.attribute(null, "type", authority.account.type);
- out.attribute(null, "authority", authority.authority);
out.attribute(null, XML_ATTR_ENABLED, Boolean.toString(authority.enabled));
+ if (authority.service == null) {
+ out.attribute(null, "account", authority.account.name);
+ out.attribute(null, "type", authority.account.type);
+ out.attribute(null, "authority", authority.authority);
+ } else {
+ out.attribute(null, "package", authority.service.getPackageName());
+ out.attribute(null, "class", authority.service.getClassName());
+ }
if (authority.syncable < 0) {
out.attribute(null, "syncable", "unknown");
} else {
out.attribute(null, "syncable", Boolean.toString(authority.syncable != 0));
}
- for (Pair<Bundle, Long> periodicSync : authority.periodicSyncs) {
+ for (PeriodicSync periodicSync : authority.periodicSyncs) {
out.startTag(null, "periodicSync");
- out.attribute(null, "period", Long.toString(periodicSync.second));
- final Bundle extras = periodicSync.first;
- for (String key : extras.keySet()) {
- out.startTag(null, "extra");
- out.attribute(null, "name", key);
- final Object value = extras.get(key);
- if (value instanceof Long) {
- out.attribute(null, "type", "long");
- out.attribute(null, "value1", value.toString());
- } else if (value instanceof Integer) {
- out.attribute(null, "type", "integer");
- out.attribute(null, "value1", value.toString());
- } else if (value instanceof Boolean) {
- out.attribute(null, "type", "boolean");
- out.attribute(null, "value1", value.toString());
- } else if (value instanceof Float) {
- out.attribute(null, "type", "float");
- out.attribute(null, "value1", value.toString());
- } else if (value instanceof Double) {
- out.attribute(null, "type", "double");
- out.attribute(null, "value1", value.toString());
- } else if (value instanceof String) {
- out.attribute(null, "type", "string");
- out.attribute(null, "value1", value.toString());
- } else if (value instanceof Account) {
- out.attribute(null, "type", "account");
- out.attribute(null, "value1", ((Account)value).name);
- out.attribute(null, "value2", ((Account)value).type);
- }
- out.endTag(null, "extra");
- }
+ out.attribute(null, "period", Long.toString(periodicSync.period));
+ out.attribute(null, "flex", Long.toString(periodicSync.flexTime));
+ final Bundle extras = periodicSync.extras;
+ extrasToXml(out, extras);
out.endTag(null, "periodicSync");
}
out.endTag(null, "authority");
}
-
out.endTag(null, "accounts");
-
out.endDocument();
-
mAccountInfoFile.finishWrite(fos);
} catch (java.io.IOException e1) {
Log.w(TAG, "Error writing accounts", e1);
@@ -2072,7 +2211,7 @@
}
}
- public static final int PENDING_OPERATION_VERSION = 3;
+ public static final int PENDING_OPERATION_VERSION = 4;
/**
* Read all pending operations back in to the initial engine state.
@@ -2080,128 +2219,162 @@
private void readPendingOperationsLocked() {
if (DEBUG_FILE) Log.v(TAG, "Reading " + mPendingFile.getBaseFile());
try {
- byte[] data = mPendingFile.readFully();
- Parcel in = Parcel.obtain();
- in.unmarshall(data, 0, data.length);
- in.setDataPosition(0);
- final int SIZE = in.dataSize();
- while (in.dataPosition() < SIZE) {
- int version = in.readInt();
- if (version != PENDING_OPERATION_VERSION && version != 1) {
- Log.w(TAG, "Unknown pending operation version "
- + version + "; dropping all ops");
- break;
- }
- int authorityId = in.readInt();
- int syncSource = in.readInt();
- byte[] flatExtras = in.createByteArray();
- boolean expedited;
- if (version == PENDING_OPERATION_VERSION) {
- expedited = in.readInt() != 0;
- } else {
- expedited = false;
- }
- int reason = in.readInt();
- AuthorityInfo authority = mAuthorities.get(authorityId);
- if (authority != null) {
- Bundle extras;
- if (flatExtras != null) {
- extras = unflattenBundle(flatExtras);
- } else {
- // if we are unable to parse the extras for whatever reason convert this
- // to a regular sync by creating an empty extras
- extras = new Bundle();
- }
- PendingOperation op = new PendingOperation(
- authority.account, authority.userId, reason, syncSource,
- authority.authority, extras, expedited);
- op.authorityId = authorityId;
- op.flatExtras = flatExtras;
- if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account
- + " auth=" + op.authority
- + " src=" + op.syncSource
- + " reason=" + op.reason
- + " expedited=" + op.expedited
- + " extras=" + op.extras);
- mPendingOperations.add(op);
- }
- }
- } catch (java.io.IOException e) {
- Log.i(TAG, "No initial pending operations");
- }
- }
-
- private void writePendingOperationLocked(PendingOperation op, Parcel out) {
- out.writeInt(PENDING_OPERATION_VERSION);
- out.writeInt(op.authorityId);
- out.writeInt(op.syncSource);
- if (op.flatExtras == null && op.extras != null) {
- op.flatExtras = flattenBundle(op.extras);
- }
- out.writeByteArray(op.flatExtras);
- out.writeInt(op.expedited ? 1 : 0);
- out.writeInt(op.reason);
- }
-
- /**
- * Write all currently pending ops to the pending ops file.
- */
- private void writePendingOperationsLocked() {
- final int N = mPendingOperations.size();
- FileOutputStream fos = null;
- try {
- if (N == 0) {
- if (DEBUG_FILE) Log.v(TAG, "Truncating " + mPendingFile.getBaseFile());
- mPendingFile.truncate();
- return;
- }
-
- if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile());
- fos = mPendingFile.startWrite();
-
- Parcel out = Parcel.obtain();
- for (int i=0; i<N; i++) {
- PendingOperation op = mPendingOperations.get(i);
- writePendingOperationLocked(op, out);
- }
- fos.write(out.marshall());
- out.recycle();
-
- mPendingFile.finishWrite(fos);
- } catch (java.io.IOException e1) {
- Log.w(TAG, "Error writing pending operations", e1);
- if (fos != null) {
- mPendingFile.failWrite(fos);
- }
- }
- }
-
- /**
- * Append the given operation to the pending ops file; if unable to,
- * write all pending ops.
- */
- private void appendPendingOperationLocked(PendingOperation op) {
- if (DEBUG_FILE) Log.v(TAG, "Appending to " + mPendingFile.getBaseFile());
- FileOutputStream fos = null;
- try {
- fos = mPendingFile.openAppend();
- } catch (java.io.IOException e) {
- if (DEBUG_FILE) Log.v(TAG, "Failed append; writing full file");
- writePendingOperationsLocked();
- return;
- }
-
- try {
- Parcel out = Parcel.obtain();
- writePendingOperationLocked(op, out);
- fos.write(out.marshall());
- out.recycle();
- } catch (java.io.IOException e1) {
- Log.w(TAG, "Error writing pending operations", e1);
- } finally {
+ readPendingAsXml();
+ } catch (XmlPullParserException e) {
+ Log.d(TAG, "Error parsing pending as xml, trying as parcel.");
try {
- fos.close();
- } catch (java.io.IOException e2) {
+ readPendingAsParcelled();
+ } catch (java.io.IOException e1) {
+ Log.i(TAG, "No initial pending operations");
+ }
+ }
+ }
+
+ private void readPendingAsXml() throws XmlPullParserException {
+ FileInputStream fis = null;
+ try {
+ Log.v(TAG, "is this thing on");
+ fis = mPendingFile.openRead();
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(fis, null);
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.START_TAG &&
+ eventType != XmlPullParser.END_DOCUMENT) {
+ eventType = parser.next();
+ Log.v(TAG, "go: " + eventType);
+ }
+ if (eventType == XmlPullParser.END_DOCUMENT) return;
+
+ String tagName = parser.getName();
+ if (DEBUG_FILE) {
+ Log.v(TAG, "got " + tagName);
+ }
+ if ("pending".equals(tagName)) {
+ int version = -1;
+ String versionString = parser.getAttributeValue(null, "version");
+ if (versionString == null ||
+ Integer.parseInt(versionString) != PENDING_OPERATION_VERSION) {
+ Log.w(TAG, "Unknown pending operation version "
+ + version + "; trying to read as binary.");
+ throw new XmlPullParserException("Unknown version.");
+ }
+ eventType = parser.next();
+ PendingOperation pop = null;
+ do {
+ if (DEBUG_FILE) {
+ Log.v(TAG, "parsing xml file");
+ }
+ if (eventType == XmlPullParser.START_TAG) {
+ try {
+ tagName = parser.getName();
+ if (parser.getDepth() == 2 && "op".equals(tagName)) {
+ int authorityId = Integer.valueOf(parser.getAttributeValue(
+ null, XML_ATTR_AUTHORITYID));
+ boolean expedited = Boolean.valueOf(parser.getAttributeValue(
+ null, XML_ATTR_EXPEDITED));
+ int syncSource = Integer.valueOf(parser.getAttributeValue(
+ null, XML_ATTR_SOURCE));
+ int reason = Integer.valueOf(parser.getAttributeValue(
+ null, XML_ATTR_REASON));
+ AuthorityInfo authority = mAuthorities.get(authorityId);
+ if (DEBUG_FILE) {
+ Log.v(TAG, authorityId + " " + expedited + " " + syncSource + " " + reason);
+ }
+ if (authority != null) {
+ pop = new PendingOperation(
+ authority.account, authority.userId, reason, syncSource,
+ authority.authority, new Bundle(), expedited);
+ pop.authorityId = authorityId;
+ pop.flatExtras = null; // No longer used.
+ mPendingOperations.add(pop);
+ if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + pop.account
+ + " auth=" + pop.authority
+ + " src=" + pop.syncSource
+ + " reason=" + pop.reason
+ + " expedited=" + pop.expedited);
+ } else {
+ // Skip non-existent authority;
+ pop = null;
+ if (DEBUG_FILE) {
+ Log.v(TAG, "No authority found for " + authorityId
+ + ", skipping");
+ }
+ }
+ } else if (parser.getDepth() == 3 &&
+ pop != null &&
+ "extra".equals(tagName)) {
+ parseExtra(parser, pop.extras);
+ }
+ } catch (NumberFormatException e) {
+ Log.d(TAG, "Invalid data in xml file.", e);
+ }
+ }
+ eventType = parser.next();
+ } while(eventType != XmlPullParser.END_DOCUMENT);
+ }
+ } catch (java.io.IOException e) {
+ if (fis == null) Log.i(TAG, "No initial pending operations.");
+ else Log.w(TAG, "Error reading pending data.", e);
+ return;
+ } finally {
+ if (DEBUG_FILE) Log.v(TAG, "Done reading pending ops");
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (java.io.IOException e1) {}
+ }
+ }
+ }
+ /**
+ * Old format of reading pending.bin as a parcelled file. Replaced in lieu of JSON because
+ * persisting parcels is unsafe.
+ * @throws java.io.IOException
+ */
+ private void readPendingAsParcelled() throws java.io.IOException {
+ byte[] data = mPendingFile.readFully();
+ Parcel in = Parcel.obtain();
+ in.unmarshall(data, 0, data.length);
+ in.setDataPosition(0);
+ final int SIZE = in.dataSize();
+ while (in.dataPosition() < SIZE) {
+ int version = in.readInt();
+ if (version != 3 && version != 1) {
+ Log.w(TAG, "Unknown pending operation version "
+ + version + "; dropping all ops");
+ break;
+ }
+ int authorityId = in.readInt();
+ int syncSource = in.readInt();
+ byte[] flatExtras = in.createByteArray();
+ boolean expedited;
+ if (version == PENDING_OPERATION_VERSION) {
+ expedited = in.readInt() != 0;
+ } else {
+ expedited = false;
+ }
+ int reason = in.readInt();
+ AuthorityInfo authority = mAuthorities.get(authorityId);
+ if (authority != null) {
+ Bundle extras;
+ if (flatExtras != null) {
+ extras = unflattenBundle(flatExtras);
+ } else {
+ // if we are unable to parse the extras for whatever reason convert this
+ // to a regular sync by creating an empty extras
+ extras = new Bundle();
+ }
+ PendingOperation op = new PendingOperation(
+ authority.account, authority.userId, reason, syncSource,
+ authority.authority, extras, expedited);
+ op.authorityId = authorityId;
+ op.flatExtras = flatExtras;
+ if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account
+ + " auth=" + op.authority
+ + " src=" + op.syncSource
+ + " reason=" + op.reason
+ + " expedited=" + op.expedited
+ + " extras=" + op.extras);
+ mPendingOperations.add(op);
}
}
}
@@ -2235,6 +2408,115 @@
return bundle;
}
+ private static final String XML_ATTR_AUTHORITYID = "authority_id";
+ private static final String XML_ATTR_SOURCE = "source";
+ private static final String XML_ATTR_EXPEDITED = "expedited";
+ private static final String XML_ATTR_REASON = "reason";
+ /**
+ * Write all currently pending ops to the pending ops file. TODO: Change this from xml
+ * so that we can append to this file as before.
+ */
+ private void writePendingOperationsLocked() {
+ final int N = mPendingOperations.size();
+ FileOutputStream fos = null;
+ try {
+ if (N == 0) {
+ if (DEBUG_FILE) Log.v(TAG, "Truncating " + mPendingFile.getBaseFile());
+ mPendingFile.truncate();
+ return;
+ }
+ if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile());
+ fos = mPendingFile.startWrite();
+ XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(fos, "utf-8");
+ out.startDocument(null, true);
+ out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+
+ out.startTag(null, "pending");
+ out.attribute(null, "version", Integer.toString(PENDING_OPERATION_VERSION));
+
+ for (int i = 0; i < N; i++) {
+ PendingOperation pop = mPendingOperations.get(i);
+ out.startTag(null, "op");
+ out.attribute(null, XML_ATTR_AUTHORITYID, Integer.toString(pop.authorityId));
+ out.attribute(null, XML_ATTR_SOURCE, Integer.toString(pop.syncSource));
+ out.attribute(null, XML_ATTR_EXPEDITED, Boolean.toString(pop.expedited));
+ out.attribute(null, XML_ATTR_REASON, Integer.toString(pop.reason));
+ extrasToXml(out, pop.extras);
+ out.endTag(null, "op");
+ }
+ out.endTag(null, "pending");
+ out.endDocument();
+ mPendingFile.finishWrite(fos);
+ } catch (java.io.IOException e1) {
+ Log.w(TAG, "Error writing pending operations", e1);
+ if (fos != null) {
+ mPendingFile.failWrite(fos);
+ }
+ }
+ }
+
+ private void extrasToXml(XmlSerializer out, Bundle extras) throws java.io.IOException {
+ for (String key : extras.keySet()) {
+ out.startTag(null, "extra");
+ out.attribute(null, "name", key);
+ final Object value = extras.get(key);
+ if (value instanceof Long) {
+ out.attribute(null, "type", "long");
+ out.attribute(null, "value1", value.toString());
+ } else if (value instanceof Integer) {
+ out.attribute(null, "type", "integer");
+ out.attribute(null, "value1", value.toString());
+ } else if (value instanceof Boolean) {
+ out.attribute(null, "type", "boolean");
+ out.attribute(null, "value1", value.toString());
+ } else if (value instanceof Float) {
+ out.attribute(null, "type", "float");
+ out.attribute(null, "value1", value.toString());
+ } else if (value instanceof Double) {
+ out.attribute(null, "type", "double");
+ out.attribute(null, "value1", value.toString());
+ } else if (value instanceof String) {
+ out.attribute(null, "type", "string");
+ out.attribute(null, "value1", value.toString());
+ } else if (value instanceof Account) {
+ out.attribute(null, "type", "account");
+ out.attribute(null, "value1", ((Account)value).name);
+ out.attribute(null, "value2", ((Account)value).type);
+ }
+ out.endTag(null, "extra");
+ }
+ }
+
+// /**
+// * Update the pending ops file, if e
+// */
+// private void appendPendingOperationLocked(PendingOperation op) {
+// if (DEBUG_FILE) Log.v(TAG, "Appending to " + mPendingFile.getBaseFile());
+// FileOutputStream fos = null;
+// try {
+// fos = mPendingFile.openAppend();
+// } catch (java.io.IOException e) {
+// if (DEBUG_FILE) Log.v(TAG, "Failed append; writing full file");
+// writePendingOperationsLocked();
+// return;
+// }
+//
+// try {
+// Parcel out = Parcel.obtain();
+// writePendingOperationLocked(op, out);
+// fos.write(out.marshall());
+// out.recycle();
+// } catch (java.io.IOException e1) {
+// Log.w(TAG, "Error writing pending operations", e1);
+// } finally {
+// try {
+// fos.close();
+// } catch (java.io.IOException e2) {
+// }
+// }
+// }
+
private void requestSync(Account account, int userId, int reason, String authority,
Bundle extras) {
// If this is happening in the system process, then call the syncrequest listener
@@ -2330,4 +2612,18 @@
}
}
}
+
+ /**
+ * Dump state of PendingOperations.
+ */
+ public void dumpPendingOperations(StringBuilder sb) {
+ sb.append("Pending Ops: ").append(mPendingOperations.size()).append(" operation(s)\n");
+ for (PendingOperation pop : mPendingOperations) {
+ sb.append("(" + pop.account)
+ .append(", " + pop.userId)
+ .append(", " + pop.authority)
+ .append(", " + pop.extras)
+ .append(")\n");
+ }
+ }
}