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/Android.mk b/Android.mk
index 98bd88d..1fa8794 100644
--- a/Android.mk
+++ b/Android.mk
@@ -104,6 +104,7 @@
 	core/java/android/content/IIntentReceiver.aidl \
 	core/java/android/content/IIntentSender.aidl \
 	core/java/android/content/IOnPrimaryClipChangedListener.aidl \
+	core/java/android/content/IAnonymousSyncAdapter.aidl \
 	core/java/android/content/ISyncAdapter.aidl \
 	core/java/android/content/ISyncContext.aidl \
 	core/java/android/content/ISyncStatusObserver.aidl \
@@ -339,6 +340,7 @@
 	frameworks/base/core/java/android/content/Intent.aidl \
 	frameworks/base/core/java/android/content/IntentSender.aidl \
 	frameworks/base/core/java/android/content/PeriodicSync.aidl \
+	frameworks/base/core/java/android/content/SyncRequest.aidl \
 	frameworks/base/core/java/android/content/SyncStats.aidl \
 	frameworks/base/core/java/android/content/res/Configuration.aidl \
 	frameworks/base/core/java/android/database/CursorWindow.aidl \
diff --git a/api/current.txt b/api/current.txt
index ee0c395..d675ab0 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5572,6 +5572,7 @@
     method public final android.os.Bundle call(android.net.Uri, java.lang.String, java.lang.String, android.os.Bundle);
     method public deprecated void cancelSync(android.net.Uri);
     method public static void cancelSync(android.accounts.Account, java.lang.String);
+    method public static void cancelSync(android.content.SyncRequest);
     method public final int delete(android.net.Uri, java.lang.String, java.lang.String[]);
     method public static deprecated android.content.SyncInfo getCurrentSync();
     method public static java.util.List<android.content.SyncInfo> getCurrentSyncs();
@@ -5599,6 +5600,7 @@
     method public static void removePeriodicSync(android.accounts.Account, java.lang.String, android.os.Bundle);
     method public static void removeStatusChangeListener(java.lang.Object);
     method public static void requestSync(android.accounts.Account, java.lang.String, android.os.Bundle);
+    method public static void requestSync(android.content.SyncRequest);
     method public static void setIsSyncable(android.accounts.Account, java.lang.String, int);
     method public static void setMasterSyncAutomatically(boolean);
     method public static void setSyncAutomatically(android.accounts.Account, java.lang.String, boolean);
@@ -6534,7 +6536,9 @@
     field public final android.accounts.Account account;
     field public final java.lang.String authority;
     field public final android.os.Bundle extras;
+    field public final boolean isService;
     field public final long period;
+    field public final android.content.ComponentName service;
   }
 
   public class ReceiverCallNotAllowedException extends android.util.AndroidRuntimeException {
@@ -6653,6 +6657,31 @@
     field public final long startTime;
   }
 
+  public class SyncRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method public boolean isExpedited();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator CREATOR;
+  }
+
+  public static class SyncRequest.Builder {
+    ctor public SyncRequest.Builder();
+    method public android.content.SyncRequest build();
+    method public android.content.SyncRequest.Builder setAllowMetered(boolean);
+    method public android.content.SyncRequest.Builder setExpedited(boolean);
+    method public android.content.SyncRequest.Builder setExtras(android.os.Bundle);
+    method public android.content.SyncRequest.Builder setIgnoreBackoff(boolean);
+    method public android.content.SyncRequest.Builder setIgnoreSettings(boolean);
+    method public android.content.SyncRequest.Builder setManual(boolean);
+    method public android.content.SyncRequest.Builder setNoRetry(boolean);
+    method public android.content.SyncRequest.Builder setPriority(int);
+    method public android.content.SyncRequest.Builder setSyncAdapter(android.accounts.Account, java.lang.String);
+    method public android.content.SyncRequest.Builder setSyncAdapter(android.content.ComponentName);
+    method public android.content.SyncRequest.Builder setTransferSize(long, long);
+    method public android.content.SyncRequest.Builder syncOnce(long, long);
+    method public android.content.SyncRequest.Builder syncPeriodic(long, long);
+  }
+
   public final class SyncResult implements android.os.Parcelable {
     ctor public SyncResult();
     method public void clear();
@@ -6676,6 +6705,12 @@
     field public boolean tooManyRetries;
   }
 
+  public abstract class SyncService extends android.app.Service {
+    ctor public SyncService();
+    method public android.os.IBinder onBind(android.content.Intent);
+    method public abstract void onPerformSync(android.os.Bundle, android.content.SyncResult);
+  }
+
   public class SyncStats implements android.os.Parcelable {
     ctor public SyncStats();
     ctor public SyncStats(android.os.Parcel);
diff --git a/core/java/android/content/AbstractThreadedSyncAdapter.java b/core/java/android/content/AbstractThreadedSyncAdapter.java
index bafe67d..613450b 100644
--- a/core/java/android/content/AbstractThreadedSyncAdapter.java
+++ b/core/java/android/content/AbstractThreadedSyncAdapter.java
@@ -147,6 +147,7 @@
     }
 
     private class ISyncAdapterImpl extends ISyncAdapter.Stub {
+        @Override
         public void startSync(ISyncContext syncContext, String authority, Account account,
                 Bundle extras) {
             final SyncContext syncContextClient = new SyncContext(syncContext);
@@ -184,6 +185,7 @@
             }
         }
 
+        @Override
         public void cancelSync(ISyncContext syncContext) {
             // synchronize to make sure that mSyncThreads doesn't change between when we
             // check it and when we use it
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index f090e07..243c91a 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -44,6 +44,7 @@
 import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.Log;
+import android.util.Pair;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -131,6 +132,19 @@
      */
     public static final String SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS = "discard_deletions";
 
+    /* Extensions to API. TODO: Not clear if we will keep these as public flags. */
+    /** {@hide} User-specified flag for expected upload size. */
+    public static final String SYNC_EXTRAS_EXPECTED_UPLOAD = "expected_upload";
+
+    /** {@hide} User-specified flag for expected download size. */
+    public static final String SYNC_EXTRAS_EXPECTED_DOWNLOAD = "expected_download";
+
+    /** {@hide} Priority of this sync with respect to other syncs scheduled for this application. */
+    public static final String SYNC_EXTRAS_PRIORITY = "sync_priority";
+
+    /** {@hide} Flag to allow sync to occur on metered network. */
+    public static final String SYNC_EXTRAS_ALLOW_METERED = "allow_metered";
+
     /**
      * Set by the SyncManager to request that the SyncAdapter initialize itself for
      * the given account/authority pair. One required initialization step is to
@@ -1385,6 +1399,8 @@
      * <li>Float</li>
      * <li>Double</li>
      * <li>String</li>
+     * <li>Account</li>
+     * <li>null</li>
      * </ul>
      *
      * @param uri the uri of the provider to sync or null to sync all providers.
@@ -1416,6 +1432,8 @@
      * <li>Float</li>
      * <li>Double</li>
      * <li>String</li>
+     * <li>Account</li>
+     * <li>null</li>
      * </ul>
      *
      * @param account which account should be synced
@@ -1423,10 +1441,24 @@
      * @param extras any extras to pass to the SyncAdapter.
      */
     public static void requestSync(Account account, String authority, Bundle extras) {
-        validateSyncExtrasBundle(extras);
+        SyncRequest request =
+            new SyncRequest.Builder()
+                .setSyncAdapter(account, authority)
+                .setExtras(extras)
+                .syncOnce(0, 0)     // Immediate sync.
+                .build();
+        requestSync(request);
+    }
+
+    /**
+     * Register a sync with the SyncManager. These requests are built using the
+     * {@link SyncRequest.Builder}.
+     */
+    public static void requestSync(SyncRequest request) {
         try {
-            getContentService().requestSync(account, authority, extras);
-        } catch (RemoteException e) {
+            getContentService().sync(request);
+        } catch(RemoteException e) {
+            // Shouldn't happen.
         }
     }
 
@@ -1586,7 +1618,7 @@
             throw new IllegalArgumentException("illegal extras were set");
         }
         try {
-            getContentService().addPeriodicSync(account, authority, extras, pollFrequency);
+             getContentService().addPeriodicSync(account, authority, extras, pollFrequency);
         } catch (RemoteException e) {
             // exception ignored; if this is thrown then it means the runtime is in the midst of
             // being restarted
@@ -1619,6 +1651,22 @@
     }
 
     /**
+     * Remove the specified sync. This will remove any syncs that have been scheduled to run, but
+     * will not cancel any running syncs.
+     * <p>This method requires the caller to hold the permission</p>
+     * If the request is for a periodic sync this will cancel future occurrences of the sync.
+     *
+     * It is possible to cancel a sync using a SyncRequest object that is different from the object
+     * with which you requested the sync. Do so by building a SyncRequest with exactly the same
+     * service/adapter, frequency, <b>and</b> extras bundle.
+     *
+     * @param request SyncRequest object containing information about sync to cancel.
+     */
+    public static void cancelSync(SyncRequest request) {
+        // TODO: Finish this implementation.
+    }
+
+    /**
      * Get the list of information about the periodic syncs for the given account and authority.
      * <p>This method requires the caller to hold the permission
      * {@link android.Manifest.permission#READ_SYNC_SETTINGS}.
diff --git a/core/java/android/content/IAnonymousSyncAdapter.aidl b/core/java/android/content/IAnonymousSyncAdapter.aidl
new file mode 100644
index 0000000..a80cea3
--- /dev/null
+++ b/core/java/android/content/IAnonymousSyncAdapter.aidl
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 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 android.os.Bundle;
+import android.content.ISyncContext;
+
+/**
+ * Interface to define an anonymous service that is extended by developers
+ * in order to perform anonymous syncs (syncs without an Account or Content
+ * Provider specified). See {@link android.content.AbstractThreadedSyncAdapter}.
+ * {@hide}
+ */
+oneway interface IAnonymousSyncAdapter {
+
+    /**
+     * Initiate a sync. SyncAdapter-specific parameters may be specified in
+     * extras, which is guaranteed to not be null.
+     *
+     * @param syncContext the ISyncContext used to indicate the progress of the sync. When
+     *   the sync is finished (successfully or not) ISyncContext.onFinished() must be called.
+     * @param extras SyncAdapter-specific parameters.
+     *
+     */
+    void startSync(ISyncContext syncContext, in Bundle extras);
+
+    /**
+     * Cancel the currently ongoing sync.
+     */
+    void cancelSync(ISyncContext syncContext);
+
+}
diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl
index f956bcf..9ad5a19 100644
--- a/core/java/android/content/IContentService.aidl
+++ b/core/java/android/content/IContentService.aidl
@@ -20,6 +20,7 @@
 import android.content.SyncInfo;
 import android.content.ISyncStatusObserver;
 import android.content.SyncAdapterType;
+import android.content.SyncRequest;
 import android.content.SyncStatusInfo;
 import android.content.PeriodicSync;
 import android.net.Uri;
@@ -54,6 +55,7 @@
             int userHandle);
 
     void requestSync(in Account account, String authority, in Bundle extras);
+    void sync(in SyncRequest request);
     void cancelSync(in Account account, String authority);
 
     /**
diff --git a/core/java/android/content/PeriodicSync.java b/core/java/android/content/PeriodicSync.java
index 513a556..6aca151 100644
--- a/core/java/android/content/PeriodicSync.java
+++ b/core/java/android/content/PeriodicSync.java
@@ -22,67 +22,170 @@
 import android.accounts.Account;
 
 /**
- * Value type that contains information about a periodic sync. Is parcelable, making it suitable
- * for passing in an IPC.
+ * Value type that contains information about a periodic sync.
  */
 public class PeriodicSync implements Parcelable {
-    /** The account to be synced */
+    /** The account to be synced. Can be null. */
     public final Account account;
-    /** The authority of the sync */
+    /** The authority of the sync. Can be null. */
     public final String authority;
+    /** The service for syncing, if this is an anonymous sync. Can be null.*/
+    public final ComponentName service;
     /** Any extras that parameters that are to be passed to the sync adapter. */
     public final Bundle extras;
-    /** How frequently the sync should be scheduled, in seconds. */
+    /** How frequently the sync should be scheduled, in seconds. Kept around for API purposes. */
     public final long period;
+    /** Whether this periodic sync uses a service. */
+    public final boolean isService;
+    /**
+     * How much flexibility can be taken in scheduling the sync, in seconds.
+     * {@hide}
+     */
+    public final long flexTime;
 
-    /** Creates a new PeriodicSync, copying the Bundle */
-    public PeriodicSync(Account account, String authority, Bundle extras, long period) {
+      /**
+       * Creates a new PeriodicSync, copying the Bundle. SM no longer uses this ctor - kept around
+       * becuse it is part of the API.
+       * Note - even calls to the old API will not use this ctor, as
+       * they are given a default flex time.
+       */
+    public PeriodicSync(Account account, String authority, Bundle extras, long periodInSeconds) {
         this.account = account;
         this.authority = authority;
-        this.extras = new Bundle(extras);
-        this.period = period;
+        this.service = null;
+        this.isService = false;
+        if (extras == null) {
+            this.extras = new Bundle();
+        } else {
+            this.extras = new Bundle(extras);
+        }
+        this.period = periodInSeconds;
+        // Old API uses default flex time. No-one should be using this ctor anyway.
+        this.flexTime = 0L;
     }
 
+    // TODO: Add copy ctor from SyncRequest?
+
+    /**
+     * Create a copy of a periodic sync.
+     * {@hide}
+     */
+    public PeriodicSync(PeriodicSync other) {
+        this.account = other.account;
+        this.authority = other.authority;
+        this.service = other.service;
+        this.isService = other.isService;
+        this.extras = new Bundle(other.extras);
+        this.period = other.period;
+        this.flexTime = other.flexTime;
+    }
+
+    /**
+     * A PeriodicSync for a sync with a specified provider.
+     * {@hide}
+     */
+    public PeriodicSync(Account account, String authority, Bundle extras,
+            long period, long flexTime) {
+        this.account = account;
+        this.authority = authority;
+        this.service = null;
+        this.isService = false;
+        this.extras = new Bundle(extras);
+        this.period = period;
+        this.flexTime = flexTime;
+    }
+
+    /**
+     * A PeriodicSync for a sync with a specified SyncService.
+     * {@hide}
+     */
+    public PeriodicSync(ComponentName service, Bundle extras,
+            long period,
+            long flexTime) {
+        this.account = null;
+        this.authority = null;
+        this.service = service;
+        this.isService = true;
+        this.extras = new Bundle(extras);
+        this.period = period;
+        this.flexTime = flexTime;
+    }
+
+    private PeriodicSync(Parcel in) {
+        this.isService = (in.readInt() != 0);
+        if (this.isService) {
+            this.service = in.readParcelable(null);
+            this.account = null;
+            this.authority = null;
+        } else {
+            this.account = in.readParcelable(null);
+            this.authority = in.readString();
+            this.service = null;
+        }
+        this.extras = in.readBundle();
+        this.period = in.readLong();
+        this.flexTime = in.readLong();
+    }
+
+    @Override
     public int describeContents() {
         return 0;
     }
 
+    @Override
     public void writeToParcel(Parcel dest, int flags) {
-        account.writeToParcel(dest, flags);
-        dest.writeString(authority);
+        dest.writeInt(isService ? 1 : 0);
+        if (account == null && authority == null) {
+            dest.writeParcelable(service, flags);
+        } else {
+            dest.writeParcelable(account, flags);
+            dest.writeString(authority);
+        }
         dest.writeBundle(extras);
         dest.writeLong(period);
+        dest.writeLong(flexTime);
     }
 
     public static final Creator<PeriodicSync> CREATOR = new Creator<PeriodicSync>() {
+        @Override
         public PeriodicSync createFromParcel(Parcel source) {
-            return new PeriodicSync(Account.CREATOR.createFromParcel(source),
-                    source.readString(), source.readBundle(), source.readLong());
+            return new PeriodicSync(source);
         }
 
+        @Override
         public PeriodicSync[] newArray(int size) {
             return new PeriodicSync[size];
         }
     };
 
+    @Override
     public boolean equals(Object o) {
         if (o == this) {
             return true;
         }
-
         if (!(o instanceof PeriodicSync)) {
             return false;
         }
-
         final PeriodicSync other = (PeriodicSync) o;
-
-        return account.equals(other.account)
-                && authority.equals(other.authority)
-                && period == other.period
-                && syncExtrasEquals(extras, other.extras);
+        if (this.isService != other.isService) {
+            return false;
+        }
+        boolean equal = false;
+        if (this.isService) {
+            equal = service.equals(other.service);
+        } else {
+            equal = account.equals(other.account)
+                    && authority.equals(other.authority);
+        }
+        return equal
+            && period == other.period
+            && syncExtrasEquals(extras, other.extras);
     }
 
-    /** {@hide} */
+    /**
+     * Periodic sync extra comparison function.
+     * {@hide}
+     */
     public static boolean syncExtrasEquals(Bundle b1, Bundle b2) {
         if (b1.size() != b2.size()) {
             return false;
@@ -100,4 +203,13 @@
         }
         return true;
     }
+
+    @Override
+    public String toString() {
+        return "account: " + account +
+               ", authority: " + authority +
+               ", service: " + service +
+               ". period: " + period + "s " +
+               ", flex: " + flexTime;
+    }
 }
diff --git a/core/java/android/content/SyncRequest.aidl b/core/java/android/content/SyncRequest.aidl
new file mode 100644
index 0000000..8321fac
--- /dev/null
+++ b/core/java/android/content/SyncRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2013 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;
+
+parcelable SyncRequest;
diff --git a/core/java/android/content/SyncRequest.java b/core/java/android/content/SyncRequest.java
new file mode 100644
index 0000000..336371e
--- /dev/null
+++ b/core/java/android/content/SyncRequest.java
@@ -0,0 +1,625 @@
+/*
+ * Copyright (C) 2013 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 android.accounts.Account;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+public class SyncRequest implements Parcelable {
+    private static final String TAG = "SyncRequest";
+    /** Account to pass to the sync adapter. Can be null. */
+    private final Account mAccountToSync;
+    /** Authority string that corresponds to a ContentProvider. */
+    private final String mAuthority;
+    /** {@link SyncService} identifier. */
+    private final ComponentName mComponentInfo;
+    /** Bundle containing user info as well as sync settings. */
+    private final Bundle mExtras;
+    /** Allow this sync request on metered networks. */
+    private final boolean mAllowMetered;
+    /**
+     * Anticipated upload size in bytes.
+     * TODO: Not yet used - we put this information into the bundle for simplicity.
+     */
+    private final long mTxBytes;
+    /**
+     * Anticipated download size in bytes.
+     * TODO: Not yet used - we put this information into the bundle.
+     */
+    private final long mRxBytes;
+    /**
+     * Amount of time before {@link mSyncRunTimeSecs} from which the sync may optionally be
+     * started.
+     */
+    private final long mSyncFlexTimeSecs;
+    /**
+     * Specifies a point in the future at which the sync must have been scheduled to run.
+     */
+    private final long mSyncRunTimeSecs;
+    /** Periodic versus one-off. */
+    private final boolean mIsPeriodic;
+    /** Service versus provider. */
+    private final boolean mIsAuthority;
+    /** Sync should be run in lieu of other syncs. */
+    private final boolean mIsExpedited;
+
+    /**
+     * {@hide}
+     * @return whether this sync is periodic or one-time. A Sync Request must be
+     *         either one of these or an InvalidStateException will be thrown in
+     *         Builder.build().
+     */
+    public boolean isPeriodic() {
+        return mIsPeriodic;
+    }
+
+    public boolean isExpedited() {
+        return mIsExpedited;
+    }
+
+    /**
+     * {@hide}
+     * @return true if this sync uses an account/authority pair, or false if
+     *         this is an anonymous sync bound to an @link AnonymousSyncService.
+     */
+    public boolean hasAuthority() {
+        return mIsAuthority;
+    }
+
+    /**
+     * {@hide}
+     * Throws a runtime IllegalArgumentException if this function is called for an
+     * anonymous sync.
+     *
+     * @return (Account, Provider) for this SyncRequest.
+     */
+    public Pair<Account, String> getProviderInfo() {
+        if (!hasAuthority()) {
+            throw new IllegalArgumentException("Cannot getProviderInfo() for an anonymous sync.");
+        }
+        return Pair.create(mAccountToSync, mAuthority);
+    }
+
+    /**
+     * {@hide}
+     * Throws a runtime IllegalArgumentException if this function is called for a
+     * SyncRequest that is bound to an account/provider.
+     *
+     * @return ComponentName for the service that this sync will bind to.
+     */
+    public ComponentName getService() {
+        if (hasAuthority()) {
+            throw new IllegalArgumentException(
+                    "Cannot getAnonymousService() for a sync that has specified a provider.");
+        }
+        return mComponentInfo;
+    }
+
+    /**
+     * {@hide}
+     * Retrieve bundle for this SyncRequest. Will not be null.
+     */
+    public Bundle getBundle() {
+        return mExtras;
+    }
+
+    /**
+     * {@hide}
+     * @return the earliest point in time that this sync can be scheduled.
+     */
+    public long getSyncFlexTime() {
+        return mSyncFlexTimeSecs;
+    }
+    /**
+     * {@hide}
+     * @return the last point in time at which this sync must scheduled.
+     */
+    public long getSyncRunTime() {
+        return mSyncRunTimeSecs;
+    }
+
+    public static final Creator<SyncRequest> CREATOR = new Creator<SyncRequest>() {
+
+        @Override
+        public SyncRequest createFromParcel(Parcel in) {
+            return new SyncRequest(in);
+        }
+
+        @Override
+        public SyncRequest[] newArray(int size) {
+            return new SyncRequest[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeBundle(mExtras);
+        parcel.writeLong(mSyncFlexTimeSecs);
+        parcel.writeLong(mSyncRunTimeSecs);
+        parcel.writeInt((mIsPeriodic ? 1 : 0));
+        parcel.writeInt((mAllowMetered ? 1 : 0));
+        parcel.writeLong(mTxBytes);
+        parcel.writeLong(mRxBytes);
+        parcel.writeInt((mIsAuthority ? 1 : 0));
+        parcel.writeInt((mIsExpedited? 1 : 0));
+        if (mIsAuthority) {
+            parcel.writeParcelable(mAccountToSync, flags);
+            parcel.writeString(mAuthority);
+        } else {
+            parcel.writeParcelable(mComponentInfo, flags);
+        }
+    }
+
+    private SyncRequest(Parcel in) {
+        mExtras = in.readBundle();
+        mSyncFlexTimeSecs = in.readLong();
+        mSyncRunTimeSecs = in.readLong();
+        mIsPeriodic = (in.readInt() != 0);
+        mAllowMetered = (in.readInt() != 0);
+        mTxBytes = in.readLong();
+        mRxBytes = in.readLong();
+        mIsAuthority = (in.readInt() != 0);
+        mIsExpedited = (in.readInt() != 0);
+        if (mIsAuthority) {
+            mComponentInfo = null;
+            mAccountToSync = in.readParcelable(null);
+            mAuthority = in.readString();
+        } else {
+            mComponentInfo = in.readParcelable(null);
+            mAccountToSync = null;
+            mAuthority = null;
+        }
+    }
+
+    /** {@hide} Protected ctor to instantiate anonymous SyncRequest. */
+    protected SyncRequest(SyncRequest.Builder b) {
+        mSyncFlexTimeSecs = b.mSyncFlexTimeSecs;
+        mSyncRunTimeSecs = b.mSyncRunTimeSecs;
+        mAccountToSync = b.mAccount;
+        mAuthority = b.mAuthority;
+        mComponentInfo = b.mComponentName;
+        mIsPeriodic = (b.mSyncType == Builder.SYNC_TYPE_PERIODIC);
+        mIsAuthority = (b.mSyncTarget == Builder.SYNC_TARGET_ADAPTER);
+        mIsExpedited = b.mExpedited;
+        mExtras = new Bundle(b.mCustomExtras);
+        mAllowMetered = b.mAllowMetered;
+        mTxBytes = b.mTxBytes;
+        mRxBytes = b.mRxBytes;
+    }
+
+    /**
+     * Builder class for a @link SyncRequest. As you build your SyncRequest this class will also
+     * perform validation.
+     */
+    public static class Builder {
+        /** Unknown sync type. */
+        private static final int SYNC_TYPE_UNKNOWN = 0;
+        /** Specify that this is a periodic sync. */
+        private static final int SYNC_TYPE_PERIODIC = 1;
+        /** Specify that this is a one-time sync. */
+        private static final int SYNC_TYPE_ONCE = 2;
+        /** Unknown sync target. */
+        private static final int SYNC_TARGET_UNKNOWN = 0;
+        /** Specify that this is an anonymous sync. */
+        private static final int SYNC_TARGET_SERVICE = 1;
+        /** Specify that this is a sync with a provider. */
+        private static final int SYNC_TARGET_ADAPTER = 2;
+        /**
+         * Earliest point of displacement into the future at which this sync can
+         * occur.
+         */
+        private long mSyncFlexTimeSecs;
+        /** Displacement into the future at which this sync must occur. */
+        private long mSyncRunTimeSecs;
+        /**
+         * Sync configuration information - custom user data explicitly provided by the developer.
+         * This data is handed over to the sync operation.
+         */
+        private Bundle mCustomExtras;
+        /**
+         * Sync system configuration -  used to store system sync configuration. Corresponds to
+         * ContentResolver.SYNC_EXTRAS_* flags.
+         * TODO: Use this instead of dumping into one bundle. Need to decide if these flags should
+         * discriminate between equivalent syncs.
+         */
+        private Bundle mSyncConfigExtras;
+        /** Expected upload transfer in bytes. */
+        private long mTxBytes = -1L;
+        /** Expected download transfer in bytes. */
+        private long mRxBytes = -1L;
+        /** Whether or not this sync can occur on metered networks. Default false. */
+        private boolean mAllowMetered;
+        /** Priority of this sync relative to others from calling app [-2, 2]. Default 0. */
+        private int mPriority = 0;
+        /**
+         * Whether this builder is building a periodic sync, or a one-time sync.
+         */
+        private int mSyncType = SYNC_TYPE_UNKNOWN;
+        /** Whether this will go to a sync adapter or to a sync service. */
+        private int mSyncTarget = SYNC_TARGET_UNKNOWN;
+        /** Whether this is a user-activated sync. */
+        private boolean mIsManual;
+        /**
+         * Whether to retry this one-time sync if the sync fails. Not valid for
+         * periodic syncs. See {@link ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY}.
+         */
+        private boolean mNoRetry;
+        /**
+         * Whether to respect back-off for this one-time sync. Not valid for
+         * periodic syncs. See
+         * {@link android.content.ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF};
+         */
+        private boolean mIgnoreBackoff;
+
+        /** Ignore sync system settings and perform sync anyway. */
+        private boolean mIgnoreSettings;
+
+        /** This sync will run in preference to other non-expedited syncs. */
+        private boolean mExpedited;
+
+        /**
+         * The @link android.content.AnonymousSyncService component that
+         * contains the sync logic if this is a provider-less sync, otherwise
+         * null.
+         */
+        private ComponentName mComponentName;
+        /**
+         * The Account object that together with an Authority name define the SyncAdapter (if
+         * this sync is bound to a provider), otherwise null. This gets resolved
+         * against a {@link com.android.server.content.SyncStorageEngine}.
+         */
+        private Account mAccount;
+        /**
+         * The Authority name that together with an Account define the SyncAdapter (if
+         * this sync is bound to a provider), otherwise null. This gets resolved
+         * against a {@link com.android.server.content.SyncStorageEngine}.
+         */
+        private String mAuthority;
+
+        public Builder() {
+        }
+
+        /**
+         * Developer can define timing constraints for this one-shot request.
+         * These values are elapsed real-time.
+         *
+         * @param whenSeconds The time in seconds at which you want this
+         *            sync to occur.
+         * @param beforeSeconds The amount of time in advance of whenSeconds that this
+         *               sync may be permitted to occur. This is rounded up to a minimum of 5
+         *               seconds, for any sync for which whenSeconds > 5.
+         *
+         * Example
+         * <pre>
+         *     Perform an immediate sync.
+         *     SyncRequest.Builder builder = (new SyncRequest.Builder()).syncOnce(0, 0);
+         *     That is, a sync 0 seconds from now with 0 seconds of flex.
+         *
+         *     Perform a sync in exactly 5 minutes.
+         *     SyncRequest.Builder builder =
+         *       new SyncRequest.Builder().syncOnce(5 * MIN_IN_SECS, 0);
+         *
+         *     Perform a sync in 5 minutes, with one minute of leeway (between 4 and 5 minutes from
+         *     now).
+         *     SyncRequest.Builder builder =
+         *       new SyncRequest.Builder().syncOnce(5 * MIN_IN_SECS, 1 * MIN_IN_SECS);
+         * </pre>
+         */
+        public Builder syncOnce(long whenSeconds, long beforeSeconds) {
+            if (mSyncType != SYNC_TYPE_UNKNOWN) {
+                throw new IllegalArgumentException("Sync type has already been defined.");
+            }
+            mSyncType = SYNC_TYPE_ONCE;
+            setupInterval(whenSeconds, beforeSeconds);
+            return this;
+        }
+
+        /**
+         * Build a periodic sync. Either this or syncOnce() <b>must</b> be called for this builder.
+         * Syncs are identified by target {@link SyncService}/{@link android.provider} and by the
+         * contents of the extras bundle.
+         * You cannot reuse the same builder for one-time syncs after having specified a periodic
+         * sync (by calling this function). If you do, an {@link IllegalArgumentException} will be
+         * thrown.
+         * 
+         * Example usage.
+         *
+         * <pre>
+         *     Request a periodic sync every 5 hours with 20 minutes of flex.
+         *     SyncRequest.Builder builder =
+         *         (new SyncRequest.Builder()).syncPeriodic(5 * HOUR_IN_SECS, 20 * MIN_IN_SECS);
+         *
+         *     Schedule a periodic sync every hour at any point in time during that hour.
+         *     SyncRequest.Builder builder =
+         *         (new SyncRequest.Builder()).syncPeriodic(1 * HOUR_IN_SECS, 1 * HOUR_IN_SECS);
+         * </pre>
+         *
+         * N.B.: Periodic syncs are not allowed to have any of
+         * {@link #SYNC_EXTRAS_DO_NOT_RETRY},
+         * {@link #SYNC_EXTRAS_IGNORE_BACKOFF},
+         * {@link #SYNC_EXTRAS_IGNORE_SETTINGS},
+         * {@link #SYNC_EXTRAS_INITIALIZE},
+         * {@link #SYNC_EXTRAS_FORCE},
+         * {@link #SYNC_EXTRAS_EXPEDITED},
+         * {@link #SYNC_EXTRAS_MANUAL}
+         * set to true. If any are supplied then an {@link IllegalArgumentException} will
+         * be thrown.
+         *
+         * @param pollFrequency the amount of time in seconds that you wish
+         *            to elapse between periodic syncs.
+         * @param beforeSeconds the amount of flex time in seconds before
+         *            {@code pollFrequency} that you permit for the sync to take
+         *            place. Must be less than {@code pollFrequency}.
+         */
+        public Builder syncPeriodic(long pollFrequency, long beforeSeconds) {
+            if (mSyncType != SYNC_TYPE_UNKNOWN) {
+                throw new IllegalArgumentException("Sync type has already been defined.");
+            }
+            mSyncType = SYNC_TYPE_PERIODIC;
+            setupInterval(pollFrequency, beforeSeconds);
+            return this;
+        }
+
+        /** {@hide} */
+        private void setupInterval(long at, long before) {
+            if (before > at) {
+                throw new IllegalArgumentException("Specified run time for the sync must be" +
+                		" after the specified flex time.");
+            }
+            mSyncRunTimeSecs = at;
+            mSyncFlexTimeSecs = before;
+        }
+
+        /**
+         * Developer can provide insight into their payload size; optional. -1 specifies
+         * unknown, so that you are not restricted to defining both fields.
+         *
+         * @param rxBytes Bytes expected to be downloaded.
+         * @param txBytes Bytes expected to be uploaded.
+         */
+        public Builder setTransferSize(long rxBytes, long txBytes) {
+            mRxBytes = rxBytes;
+            mTxBytes = txBytes;
+            return this;
+        }
+
+        /**
+         * @param allow false to allow this transfer on metered networks.
+         *            Default true.
+         */
+        public Builder setAllowMetered(boolean allow) {
+            mAllowMetered = true;
+            return this;
+        }
+
+        /**
+         * Give ourselves a concrete way of binding. Use an explicit
+         * authority+account SyncAdapter for this transfer, otherwise we bind
+         * anonymously to given componentname.
+         *
+         * @param authority
+         * @param account Account to sync. Can be null unless this is a periodic
+         *            sync, for which verification by the ContentResolver will
+         *            fail. If a sync is performed without an account, the
+         */
+        public Builder setSyncAdapter(Account account, String authority) {
+            if (mSyncTarget != SYNC_TARGET_UNKNOWN) {
+                throw new IllegalArgumentException("Sync target has already been defined.");
+            }
+            mSyncTarget = SYNC_TARGET_ADAPTER;
+            mAccount = account;
+            mAuthority = authority;
+            mComponentName = null;
+            return this;
+        }
+
+        /**
+         * Set Service component name for anonymous sync. This is not validated
+         * until sync time so providing an incorrect component name here will
+         * not fail.
+         *
+         * @param cname ComponentName to identify your Anonymous service
+         */
+        public Builder setSyncAdapter(ComponentName cname) {
+            if (mSyncTarget != SYNC_TARGET_UNKNOWN) {
+                throw new IllegalArgumentException("Sync target has already been defined.");
+            }
+            mSyncTarget = SYNC_TARGET_SERVICE;
+            mComponentName = cname;
+            mAccount = null;
+            mAuthority = null;
+            return this;
+        }
+
+        /**
+         * Developer-provided extras handed back when sync actually occurs. This bundle is copied
+         * into the SyncRequest returned by build().
+         *
+         * Example:
+         * <pre>
+         *   String[] syncItems = {"dog", "cat", "frog", "child"};
+         *   SyncRequest.Builder builder =
+         *     new SyncRequest.Builder()
+         *       .setSyncAdapter(dummyAccount, dummyProvider)
+         *       .syncOnce(5 * MINUTES_IN_SECS);
+         *
+         *   for (String syncData : syncItems) {
+         *     Bundle extras = new Bundle();
+         *     extras.setString("data", syncData);
+         *     builder.setExtras(extras);
+         *     ContentResolver.sync(builder.build()); // Each sync() request creates a unique sync.
+         *   }
+         * </pre>
+         *
+         * Only values of the following types may be used in the extras bundle:
+         * <ul>
+         * <li>Integer</li>
+         * <li>Long</li>
+         * <li>Boolean</li>
+         * <li>Float</li>
+         * <li>Double</li>
+         * <li>String</li>
+         * <li>Account</li>
+         * <li>null</li>
+         * </ul>
+         * If any data is present in the bundle not of this type, build() will
+         * throw a runtime exception.
+         *
+         * @param bundle
+         */
+        public Builder setExtras(Bundle bundle) {
+            mCustomExtras = bundle;
+            return this;
+        }
+
+        /**
+         * Convenience function for setting {@link ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY}. A
+         * one-off sync operation that fails will be retried at a later date unless this is
+         * set to false. Default is true. Not valid for periodic sync and will throw an
+         * IllegalArgumentException in Builder.build().
+         *
+         * @param retry false to not retry a failed sync. Default true.
+         */
+        public Builder setNoRetry(boolean retry) {
+            mNoRetry = retry;
+            return this;
+        }
+
+        /**
+         * {@link ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS}. Not valid for
+         * periodic sync and will throw an IllegalArgumentException in
+         * Builder.build(). Default false.
+         */
+        public Builder setIgnoreSettings(boolean ignoreSettings) {
+            mIgnoreSettings = ignoreSettings;
+            return this;
+        }
+
+        /**
+         * Convenience function for setting {@link ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF}.
+         *
+         * @param ignoreBackoff
+         */
+        public Builder setIgnoreBackoff(boolean ignoreBackoff) {
+            mIgnoreBackoff = ignoreBackoff;
+            return this;
+        }
+
+        /**
+         * {@link ContentResolver.SYNC_EXTRAS_MANUAL}. Default false.
+         */
+        public Builder setManual(boolean isManual) {
+            mIsManual = isManual;
+            return this;
+        }
+
+        /**
+         * {@link ContentResolver.SYNC_EXTRAS_} Default false.
+         */
+        public Builder setExpedited(boolean expedited) {
+            mExpedited = expedited;
+            return this;
+        }
+
+        /**
+         * Priority of this request among all requests from the calling app.
+         * Range of [-2,2] similar to {@link android.app.Notification.priority}.
+         */
+        public Builder setPriority(int priority) {
+            if (priority < -2 || priority > 2) {
+                throw new IllegalArgumentException("Priority must be within range [-2, 2]");
+            }
+            mPriority = priority;
+            return this;
+        }
+
+        /**
+         * Performs validation over the request and throws the runtime exception
+         * IllegalArgumentException if this validation fails. TODO: Add
+         * validation of SyncRequest here. 1) Cannot specify both periodic &
+         * one-off (fails above). 2) Cannot specify both service and
+         * account/provider (fails above).
+         *
+         * @return a SyncRequest with the information contained within this
+         *         builder.
+         */
+        public SyncRequest build() {
+            // Validate the extras bundle
+            try {
+                ContentResolver.validateSyncExtrasBundle(mCustomExtras);
+            } catch (IllegalArgumentException e) {
+                throw new IllegalArgumentException(e.getMessage());
+            }
+            if (mCustomExtras == null) {
+                mCustomExtras = new Bundle();
+            }
+            // Combine the builder extra flags into the copy of the bundle.
+            if (mIgnoreBackoff) {
+                mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true);
+            }
+            if (mAllowMetered) {
+                mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_ALLOW_METERED, true);
+            }
+            if (mIgnoreSettings) {
+                mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
+            }
+            if (mNoRetry) {
+                mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
+            }
+            if (mExpedited) {
+                mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+            }
+            if (mIsManual) {
+                mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+            }
+            // Upload/download expectations.
+            mCustomExtras.putLong(ContentResolver.SYNC_EXTRAS_EXPECTED_UPLOAD, mTxBytes);
+            mCustomExtras.putLong(ContentResolver.SYNC_EXTRAS_EXPECTED_DOWNLOAD, mRxBytes);
+            // Priority.
+            mCustomExtras.putInt(ContentResolver.SYNC_EXTRAS_PRIORITY, mPriority);
+            if (mSyncType == SYNC_TYPE_PERIODIC) {
+                // If this is a periodic sync ensure than invalid extras were
+                // not set.
+                if (mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)
+                        || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false)
+                        || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)
+                        || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false)
+                        || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)
+                        || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false)
+                        || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) {
+                    throw new IllegalArgumentException("Illegal extras were set");
+                }
+            } else if (mSyncType == SYNC_TYPE_UNKNOWN) {
+                throw new IllegalArgumentException("Must call either syncOnce() or syncPeriodic()");
+            }
+            // Ensure that a target for the sync has been set.
+            if (mSyncTarget == SYNC_TARGET_UNKNOWN) {
+                throw new IllegalArgumentException("Must specify an adapter with one of"
+                    + "setSyncAdapter(ComponentName) or setSyncAdapter(Account, String");
+            }
+            return new SyncRequest(this);
+        }
+    }
+}
diff --git a/core/java/android/content/SyncResult.java b/core/java/android/content/SyncResult.java
index 6cb0d02..4f86af9 100644
--- a/core/java/android/content/SyncResult.java
+++ b/core/java/android/content/SyncResult.java
@@ -181,7 +181,7 @@
      * <li> {@link SyncStats#numIoExceptions} > 0
      * <li> {@link #syncAlreadyInProgress}
      * </ul>
-     * @return true if a hard error is indicated
+     * @return true if a soft error is indicated
      */
     public boolean hasSoftError() {
         return syncAlreadyInProgress || stats.numIoExceptions > 0;
@@ -195,6 +195,11 @@
         return hasSoftError() || hasHardError();
     }
 
+    /**
+     * Convenience method for determining if the Sync should be rescheduled after failing for some
+     * reason.
+     * @return true if the SyncManager should reschedule this sync.
+     */
     public boolean madeSomeProgress() {
         return ((stats.numDeletes > 0) && !tooManyDeletions)
                 || stats.numInserts > 0
diff --git a/core/java/android/content/SyncService.java b/core/java/android/content/SyncService.java
new file mode 100644
index 0000000..100fd40
--- /dev/null
+++ b/core/java/android/content/SyncService.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2013 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 android.app.Service;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.Trace;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashMap;
+
+/**
+ * Simplified @link android.content.AbstractThreadedSyncAdapter. Folds that
+ * behaviour into a service to which the system can bind when requesting an
+ * anonymous (providerless/accountless) sync.
+ * <p>
+ * In order to perform an anonymous sync operation you must extend this service,
+ * implementing the abstract methods. This service must then be declared in the
+ * application's manifest as usual. You can use this service for other work, however you
+ * <b> must not </b> override the onBind() method unless you know what you're doing,
+ * which limits the usefulness of this service for other work.
+ *
+ * <pre>
+ * &lt;service ndroid:name=".MyAnonymousSyncService" android:permission="android.permission.SYNC" /&gt;
+ * </pre>
+ * Like @link android.content.AbstractThreadedSyncAdapter this service supports
+ * multiple syncs at the same time. Each incoming startSync() with a unique tag
+ * will spawn a thread to do the work of that sync. If startSync() is called
+ * with a tag that already exists, a SyncResult.ALREADY_IN_PROGRESS is returned.
+ * Remember that your service will spawn multiple threads if you schedule multiple syncs
+ * at once, so if you mutate local objects you must ensure synchronization.
+ */
+public abstract class SyncService extends Service {
+
+    /** SyncAdapter Instantiation that any anonymous syncs call. */
+    private final AnonymousSyncAdapterImpl mSyncAdapter = new AnonymousSyncAdapterImpl();
+
+    /** Keep track of on-going syncs, keyed by tag. */
+    @GuardedBy("mLock")
+    private final HashMap<Bundle, AnonymousSyncThread>
+            mSyncThreads = new HashMap<Bundle, AnonymousSyncThread>();
+    /** Lock object for accessing the SyncThreads HashMap. */
+    private final Object mSyncThreadLock = new Object();
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mSyncAdapter.asBinder();
+    }
+
+    /** {@hide} */
+    private class AnonymousSyncAdapterImpl extends IAnonymousSyncAdapter.Stub {
+
+        @Override
+        public void startSync(ISyncContext syncContext, Bundle extras) {
+            // Wrap the provided Sync Context because it may go away by the time
+            // we call it.
+            final SyncContext syncContextClient = new SyncContext(syncContext);
+            boolean alreadyInProgress = false;
+            synchronized (mSyncThreadLock) {
+                if (mSyncThreads.containsKey(extras)) {
+                    // Don't want to call back to SyncManager while still
+                    // holding lock.
+                    alreadyInProgress = true;
+                } else {
+                    AnonymousSyncThread syncThread = new AnonymousSyncThread(
+                            syncContextClient, extras);
+                    mSyncThreads.put(extras, syncThread);
+                    syncThread.start();
+                }
+            }
+            if (alreadyInProgress) {
+                syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS);
+            }
+        }
+
+        /**
+         * Used by the SM to cancel a specific sync using the {@link
+         * com.android.server.content.SyncManager.ActiveSyncContext} as a handle.
+         */
+        @Override
+        public void cancelSync(ISyncContext syncContext) {
+            AnonymousSyncThread runningSync = null;
+            synchronized (mSyncThreadLock) {
+                for (AnonymousSyncThread thread : mSyncThreads.values()) {
+                    if (thread.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) {
+                        runningSync = thread;
+                        break;
+                    }
+                }
+            }
+            if (runningSync != null) {
+                runningSync.interrupt();
+            }
+        }
+    }
+
+    /**
+     * {@hide}
+     * Similar to {@link android.content.AbstractThreadedSyncAdapter.SyncThread}. However while
+     * the ATSA considers an already in-progress sync to be if the account provided is currently
+     * syncing, this anonymous sync has no notion of account and therefore considers a sync unique
+     * if the provided bundle is different.
+     */
+    private class AnonymousSyncThread extends Thread {
+        private final SyncContext mSyncContext;
+        private final Bundle mExtras;
+
+        public AnonymousSyncThread(SyncContext syncContext, Bundle extras) {
+            mSyncContext = syncContext;
+            mExtras = extras;
+        }
+
+        @Override
+        public void run() {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+            Trace.traceBegin(Trace.TRACE_TAG_SYNC_MANAGER, getApplication().getPackageName());
+
+            SyncResult syncResult = new SyncResult();
+            try {
+                if (isCancelled()) {
+                    return;
+                }
+                // Run the sync based off of the provided code.
+                SyncService.this.onPerformSync(mExtras, syncResult);
+            } finally {
+                Trace.traceEnd(Trace.TRACE_TAG_SYNC_MANAGER);
+                if (!isCancelled()) {
+                    mSyncContext.onFinished(syncResult);
+                }
+                // Synchronize so that the assignment will be seen by other
+                // threads
+                // that also synchronize accesses to mSyncThreads.
+                synchronized (mSyncThreadLock) {
+                    mSyncThreads.remove(mExtras);
+                }
+            }
+        }
+
+        private boolean isCancelled() {
+            return Thread.currentThread().isInterrupted();
+        }
+    }
+
+    /**
+     * Initiate an anonymous sync using this service. SyncAdapter-specific
+     * parameters may be specified in extras, which is guaranteed to not be
+     * null.
+     */
+    public abstract void onPerformSync(Bundle extras, SyncResult syncResult);
+
+}
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");
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/content/SyncOperationTest.java b/services/tests/servicestests/src/com/android/server/content/SyncOperationTest.java
index f2772c8..37176d6 100644
--- a/services/tests/servicestests/src/com/android/server/content/SyncOperationTest.java
+++ b/services/tests/servicestests/src/com/android/server/content/SyncOperationTest.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.server;
+package com.android.server.content;
 
 import android.accounts.Account;
+import android.content.ContentResolver;
 import android.os.Bundle;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -48,7 +49,8 @@
                 SyncOperation.REASON_PERIODIC,
                 "authority1",
                 b1,
-                100,
+                100, /* run time from now*/
+                10, /* flex */
                 1000,
                 10000,
                 false);
@@ -60,6 +62,7 @@
                 "authority1",
                 b1,
                 200,
+                20,
                 2000,
                 20000,
                 false);
@@ -71,6 +74,7 @@
                 "authority2",
                 b1,
                 100,
+                10,
                 1000,
                 10000,
                 false);
@@ -82,6 +86,7 @@
                 "authority1",
                 b1,
                 100,
+                10,
                 1000,
                 10000,
                 false);
@@ -93,6 +98,7 @@
                 "authority1",
                 b2,
                 100,
+                10,
                 1000,
                 10000,
                 false);
@@ -102,4 +108,38 @@
         assertNotSame(op1.key, op4.key);
         assertNotSame(op1.key, op5.key);
     }
+
+    @SmallTest
+    public void testCompareTo() {
+        Account dummy = new Account("account1", "type1");
+        Bundle b1 = new Bundle();
+        final long unimportant = 0L;
+        long soon = 1000;
+        long soonFlex = 50;
+        long after = 1500;
+        long afterFlex = 100;
+        SyncOperation op1 = new SyncOperation(dummy, 0, 0, SyncOperation.REASON_PERIODIC,
+                "authority1", b1, soon, soonFlex, unimportant, unimportant, true);
+
+        // Interval disjoint from and after op1.
+        SyncOperation op2 = new SyncOperation(dummy, 0, 0, SyncOperation.REASON_PERIODIC,
+                "authority1", b1, after, afterFlex, unimportant, unimportant, true);
+
+        // Interval equivalent to op1, but expedited.
+        Bundle b2 = new Bundle();
+        b2.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+        SyncOperation op3 = new SyncOperation(dummy, 0, 0, 0,
+                "authority1", b2, soon, soonFlex, unimportant, unimportant, true);
+
+        // Interval overlaps but not equivalent to op1.
+        SyncOperation op4 = new SyncOperation(dummy, 0, 0, SyncOperation.REASON_PERIODIC,
+                "authority1", b1, soon + 100, soonFlex + 100, unimportant, unimportant, true);
+
+        assertTrue(op1.compareTo(op2) == -1);
+        assertTrue("less than not transitive.", op2.compareTo(op1) == 1);
+        assertTrue(op1.compareTo(op3) == 1);
+        assertTrue("greater than not transitive. ", op3.compareTo(op1) == -1);
+        assertTrue("overlapping intervals not the same.", op1.compareTo(op4) == 0);
+        assertTrue("equality not transitive.", op4.compareTo(op1) == 0);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/content/SyncStorageEngineTest.java b/services/tests/servicestests/src/com/android/server/content/SyncStorageEngineTest.java
index 8b00f2c2..dff6661 100644
--- a/services/tests/servicestests/src/com/android/server/content/SyncStorageEngineTest.java
+++ b/services/tests/servicestests/src/com/android/server/content/SyncStorageEngineTest.java
@@ -22,6 +22,7 @@
 import android.content.ContextWrapper;
 import android.content.Intent;
 import android.content.PeriodicSync;
+import android.content.res.Resources;
 import android.os.Bundle;
 import android.test.AndroidTestCase;
 import android.test.RenamingDelegatingContext;
@@ -39,10 +40,31 @@
 
 public class SyncStorageEngineTest extends AndroidTestCase {
 
+    protected Account account1;
+    protected String authority1 = "testprovider";
+    protected Bundle defaultBundle;
+    protected final int DEFAULT_USER = 0;
+    
+    MockContentResolver mockResolver;
+    SyncStorageEngine engine;
+    
     private File getSyncDir() {
         return new File(new File(getContext().getFilesDir(), "system"), "sync");
     }
 
+    @Override
+    public void setUp() {
+        account1 = new Account("a@example.com", "example.type");
+        // Default bundle.
+        defaultBundle = new Bundle();
+        defaultBundle.putInt("int_key", 0);
+        defaultBundle.putString("string_key", "hello");
+        // Set up storage engine.
+        mockResolver = new MockContentResolver();
+        engine = SyncStorageEngine.newTestInstance(
+                new TestContext(mockResolver, getContext()));
+    }
+
     /**
      * Test that we handle the case of a history row being old enough to purge before the
      * correcponding sync is finished. This can happen if the clock changes while we are syncing.
@@ -68,7 +90,25 @@
     }
 
     /**
-     * Test that we can create, remove and retrieve periodic syncs
+     * Test persistence of pending operations.
+     */
+    @MediumTest
+    public void testPending() throws Exception {
+        SyncStorageEngine.PendingOperation pop =
+                new SyncStorageEngine.PendingOperation(account1, DEFAULT_USER,
+                        SyncOperation.REASON_PERIODIC, SyncStorageEngine.SOURCE_LOCAL,
+                        authority1, defaultBundle, false);
+        
+        engine.insertIntoPending(pop);
+        // Force engine to read from disk.
+        engine.clearAndReadState();
+
+        assert(engine.getPendingOperationCount() == 1);
+    }
+
+    /**
+     * Test that we can create, remove and retrieve periodic syncs. Backwards compatibility -
+     * periodic syncs with no flex time are no longer used.
      */
     @MediumTest
     public void testPeriodics() throws Exception {
@@ -87,22 +127,19 @@
         PeriodicSync sync3 = new PeriodicSync(account1, authority, extras2, period2);
         PeriodicSync sync4 = new PeriodicSync(account2, authority, extras2, period2);
 
-        MockContentResolver mockResolver = new MockContentResolver();
-
-        SyncStorageEngine engine = SyncStorageEngine.newTestInstance(
-                new TestContext(mockResolver, getContext()));
+        
 
         removePeriodicSyncs(engine, account1, 0, authority);
         removePeriodicSyncs(engine, account2, 0, authority);
         removePeriodicSyncs(engine, account1, 1, authority);
 
         // this should add two distinct periodic syncs for account1 and one for account2
-        engine.addPeriodicSync(sync1.account, 0, sync1.authority, sync1.extras, sync1.period);
-        engine.addPeriodicSync(sync2.account, 0, sync2.authority, sync2.extras, sync2.period);
-        engine.addPeriodicSync(sync3.account, 0, sync3.authority, sync3.extras, sync3.period);
-        engine.addPeriodicSync(sync4.account, 0, sync4.authority, sync4.extras, sync4.period);
+        engine.addPeriodicSync(sync1, 0);
+        engine.addPeriodicSync(sync2, 0);
+        engine.addPeriodicSync(sync3, 0);
+        engine.addPeriodicSync(sync4, 0);
         // add a second user
-        engine.addPeriodicSync(sync2.account, 1, sync2.authority, sync2.extras, sync2.period);
+        engine.addPeriodicSync(sync2, 1);
 
         List<PeriodicSync> syncs = engine.getPeriodicSyncs(account1, 0, authority);
 
@@ -111,7 +148,7 @@
         assertEquals(sync1, syncs.get(0));
         assertEquals(sync3, syncs.get(1));
 
-        engine.removePeriodicSync(sync1.account, 0, sync1.authority, sync1.extras);
+        engine.removePeriodicSync(sync1, 0);
 
         syncs = engine.getPeriodicSyncs(account1, 0, authority);
         assertEquals(1, syncs.size());
@@ -126,13 +163,72 @@
         assertEquals(sync2, syncs.get(0));
     }
 
-    private void removePeriodicSyncs(SyncStorageEngine engine, Account account, int userId,
-            String authority) {
-        engine.setIsSyncable(account, userId, authority,
-                engine.getIsSyncable(account, 0, authority));
+    /**
+     * Test that we can create, remove and retrieve periodic syncs with a provided flex time.
+     */
+    @MediumTest
+    public void testPeriodicsV2() throws Exception {
+        final Account account1 = new Account("a@example.com", "example.type");
+        final Account account2 = new Account("b@example.com", "example.type.2");
+        final String authority = "testprovider";
+        final Bundle extras1 = new Bundle();
+        extras1.putString("a", "1");
+        final Bundle extras2 = new Bundle();
+        extras2.putString("a", "2");
+        final int period1 = 200;
+        final int period2 = 1000;
+        final int flex1 = 10;
+        final int flex2 = 100;
+
+        PeriodicSync sync1 = new PeriodicSync(account1, authority, extras1, period1, flex1);
+        PeriodicSync sync2 = new PeriodicSync(account1, authority, extras2, period1, flex1);
+        PeriodicSync sync3 = new PeriodicSync(account1, authority, extras2, period2, flex2);
+        PeriodicSync sync4 = new PeriodicSync(account2, authority, extras2, period2, flex2);
+
+        MockContentResolver mockResolver = new MockContentResolver();
+
+        SyncStorageEngine engine = SyncStorageEngine.newTestInstance(
+                new TestContext(mockResolver, getContext()));
+
+        removePeriodicSyncs(engine, account1, 0, authority);
+        removePeriodicSyncs(engine, account2, 0, authority);
+        removePeriodicSyncs(engine, account1, 1, authority);
+
+        // This should add two distinct periodic syncs for account1 and one for account2
+        engine.addPeriodicSync(sync1, 0);
+        engine.addPeriodicSync(sync2, 0);
+        engine.addPeriodicSync(sync3, 0); // Should edit sync2 and update the period.
+        engine.addPeriodicSync(sync4, 0);
+        // add a second user
+        engine.addPeriodicSync(sync2, 1);
+
+        List<PeriodicSync> syncs = engine.getPeriodicSyncs(account1, 0, authority);
+
+        assertEquals(2, syncs.size());
+
+        assertEquals(sync1, syncs.get(0));
+        assertEquals(sync3, syncs.get(1));
+
+        engine.removePeriodicSync(sync1, 0);
+
+        syncs = engine.getPeriodicSyncs(account1, 0, authority);
+        assertEquals(1, syncs.size());
+        assertEquals(sync3, syncs.get(0));
+
+        syncs = engine.getPeriodicSyncs(account2, 0, authority);
+        assertEquals(1, syncs.size());
+        assertEquals(sync4, syncs.get(0));
+
+        syncs = engine.getPeriodicSyncs(sync2.account, 1, sync2.authority);
+        assertEquals(1, syncs.size());
+        assertEquals(sync2, syncs.get(0));
+    }
+
+    private void removePeriodicSyncs(SyncStorageEngine engine, Account account, int userId, String authority) {
+        engine.setIsSyncable(account, userId, authority, engine.getIsSyncable(account, 0, authority));
         List<PeriodicSync> syncs = engine.getPeriodicSyncs(account, userId, authority);
         for (PeriodicSync sync : syncs) {
-            engine.removePeriodicSync(sync.account, userId, sync.authority, sync.extras);
+            engine.removePeriodicSync(sync, userId);
         }
     }
 
@@ -154,12 +250,14 @@
         extras2.putParcelable("g", account1);
         final int period1 = 200;
         final int period2 = 1000;
+        final int flex1 = 10;
+        final int flex2 = 100;
 
-        PeriodicSync sync1 = new PeriodicSync(account1, authority1, extras1, period1);
-        PeriodicSync sync2 = new PeriodicSync(account1, authority1, extras2, period1);
-        PeriodicSync sync3 = new PeriodicSync(account1, authority2, extras1, period1);
-        PeriodicSync sync4 = new PeriodicSync(account1, authority2, extras2, period2);
-        PeriodicSync sync5 = new PeriodicSync(account2, authority1, extras1, period1);
+        PeriodicSync sync1 = new PeriodicSync(account1, authority1, extras1, period1, flex1);
+        PeriodicSync sync2 = new PeriodicSync(account1, authority1, extras2, period1, flex1);
+        PeriodicSync sync3 = new PeriodicSync(account1, authority2, extras1, period1, flex1);
+        PeriodicSync sync4 = new PeriodicSync(account1, authority2, extras2, period2, flex2);
+        PeriodicSync sync5 = new PeriodicSync(account2, authority1, extras1, period1, flex1);
 
         MockContentResolver mockResolver = new MockContentResolver();
 
@@ -185,11 +283,11 @@
         engine.setIsSyncable(account2, 0, authority2, 0);
         engine.setSyncAutomatically(account2, 0, authority2, true);
 
-        engine.addPeriodicSync(sync1.account, 0, sync1.authority, sync1.extras, sync1.period);
-        engine.addPeriodicSync(sync2.account, 0, sync2.authority, sync2.extras, sync2.period);
-        engine.addPeriodicSync(sync3.account, 0, sync3.authority, sync3.extras, sync3.period);
-        engine.addPeriodicSync(sync4.account, 0, sync4.authority, sync4.extras, sync4.period);
-        engine.addPeriodicSync(sync5.account, 0, sync5.authority, sync5.extras, sync5.period);
+        engine.addPeriodicSync(sync1, 0);
+        engine.addPeriodicSync(sync2, 0);
+        engine.addPeriodicSync(sync3, 0);
+        engine.addPeriodicSync(sync4, 0);
+        engine.addPeriodicSync(sync5, 0);
 
         engine.writeAllState();
         engine.clearAndReadState();
@@ -220,6 +318,131 @@
     }
 
     @MediumTest
+    /**
+     * V2 introduces flex time as well as service components.
+     * @throws Exception
+     */
+    public void testAuthorityParsingV2() throws Exception {
+        final Account account = new Account("account1", "type1");
+        final String authority1 = "auth1";
+        final String authority2 = "auth2";
+        final String authority3 = "auth3";
+
+        final long dayPoll = (60 * 60 * 24);
+        final long dayFuzz = 60;
+        final long thousandSecs = 1000;
+        final long thousandSecsFuzz = 100;
+        final Bundle extras = new Bundle();
+        PeriodicSync sync1 = new PeriodicSync(account, authority1, extras, dayPoll, dayFuzz);
+        PeriodicSync sync2 = new PeriodicSync(account, authority2, extras, dayPoll, dayFuzz);
+        PeriodicSync sync3 = new PeriodicSync(account, authority3, extras, dayPoll, dayFuzz);
+        PeriodicSync sync1s = new PeriodicSync(account, authority1, extras, thousandSecs, thousandSecsFuzz);
+        PeriodicSync sync2s = new PeriodicSync(account, authority2, extras, thousandSecs, thousandSecsFuzz);
+        PeriodicSync sync3s = new PeriodicSync(account, authority3, extras, thousandSecs, thousandSecsFuzz);
+        MockContentResolver mockResolver = new MockContentResolver();
+
+        final TestContext testContext = new TestContext(mockResolver, getContext());
+
+        byte[] accountsFileData = ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<accounts version=\"2\" >\n"
+                + "<authority id=\"0\" user=\"0\" account=\"account1\" type=\"type1\" authority=\"auth1\" >"
+                + "\n<periodicSync period=\"" + dayPoll + "\" flex=\"" + dayFuzz + "\"/>"
+                + "\n</authority>"
+                + "<authority id=\"1\" user=\"0\" account=\"account1\" type=\"type1\" authority=\"auth2\" >"
+                + "\n<periodicSync period=\"" + dayPoll + "\" flex=\"" + dayFuzz + "\"/>"
+                + "\n</authority>"
+                // No user defaults to user 0 - all users.
+                + "<authority id=\"2\"            account=\"account1\" type=\"type1\" authority=\"auth3\" >"
+                + "\n<periodicSync period=\"" + dayPoll + "\" flex=\"" + dayFuzz + "\"/>"
+                + "\n</authority>"
+                + "<authority id=\"3\" user=\"1\" account=\"account1\" type=\"type1\" authority=\"auth3\" >"
+                + "\n<periodicSync period=\"" + dayPoll + "\" flex=\"" + dayFuzz + "\"/>"
+                + "\n</authority>"
+                + "</accounts>").getBytes();
+
+        File syncDir = getSyncDir();
+        syncDir.mkdirs();
+        AtomicFile accountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"));
+        FileOutputStream fos = accountInfoFile.startWrite();
+        fos.write(accountsFileData);
+        accountInfoFile.finishWrite(fos);
+
+        SyncStorageEngine engine = SyncStorageEngine.newTestInstance(testContext);
+
+        List<PeriodicSync> syncs = engine.getPeriodicSyncs(account, 0, authority1);
+        assertEquals("Got incorrect # of syncs", 1, syncs.size());
+        assertEquals(sync1, syncs.get(0));
+
+        syncs = engine.getPeriodicSyncs(account, 0, authority2);
+        assertEquals(1, syncs.size());
+        assertEquals(sync2, syncs.get(0));
+
+        syncs = engine.getPeriodicSyncs(account, 0, authority3);
+        assertEquals(1, syncs.size());
+        assertEquals(sync3, syncs.get(0));
+
+        syncs = engine.getPeriodicSyncs(account, 1, authority3);
+        assertEquals(1, syncs.size());
+        assertEquals(sync3, syncs.get(0));
+
+        // Test empty periodic data.
+        accountsFileData = ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<accounts version=\"2\">\n"
+                + "<authority id=\"0\" account=\"account1\" type=\"type1\" authority=\"auth1\" />\n"
+                + "<authority id=\"1\" account=\"account1\" type=\"type1\" authority=\"auth2\" />\n"
+                + "<authority id=\"2\" account=\"account1\" type=\"type1\" authority=\"auth3\" />\n"
+                + "</accounts>\n").getBytes();
+
+        accountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"));
+        fos = accountInfoFile.startWrite();
+        fos.write(accountsFileData);
+        accountInfoFile.finishWrite(fos);
+
+        engine.clearAndReadState();
+
+        syncs = engine.getPeriodicSyncs(account, 0, authority1);
+        assertEquals(0, syncs.size());
+
+        syncs = engine.getPeriodicSyncs(account, 0, authority2);
+        assertEquals(0, syncs.size());
+
+        syncs = engine.getPeriodicSyncs(account, 0, authority3);
+        assertEquals(0, syncs.size());
+
+        accountsFileData = ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<accounts version=\"2\">\n"
+                + "<authority id=\"0\" account=\"account1\" type=\"type1\" authority=\"auth1\">\n"
+                + "<periodicSync period=\"1000\" />\n"
+                + "</authority>"
+                + "<authority id=\"1\" account=\"account1\" type=\"type1\" authority=\"auth2\">\n"
+                + "<periodicSync period=\"1000\" />\n"
+                + "</authority>"
+                + "<authority id=\"2\" account=\"account1\" type=\"type1\" authority=\"auth3\">\n"
+                + "<periodicSync period=\"1000\" />\n"
+                + "</authority>"
+                + "</accounts>\n").getBytes();
+
+        accountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"));
+        fos = accountInfoFile.startWrite();
+        fos.write(accountsFileData);
+        accountInfoFile.finishWrite(fos);
+
+        engine.clearAndReadState();
+
+        syncs = engine.getPeriodicSyncs(account, 0, authority1);
+        assertEquals(1, syncs.size());
+        assertEquals(sync1s, syncs.get(0));
+
+        syncs = engine.getPeriodicSyncs(account, 0, authority2);
+        assertEquals(1, syncs.size());
+        assertEquals(sync2s, syncs.get(0));
+
+        syncs = engine.getPeriodicSyncs(account, 0, authority3);
+        assertEquals(1, syncs.size());
+        assertEquals(sync3s, syncs.get(0));
+    }
+
+    @MediumTest
     public void testAuthorityParsing() throws Exception {
         final Account account = new Account("account1", "type1");
         final String authority1 = "auth1";
@@ -256,7 +479,7 @@
 
         List<PeriodicSync> syncs = engine.getPeriodicSyncs(account, 0, authority1);
         assertEquals(1, syncs.size());
-        assertEquals(sync1, syncs.get(0));
+        assertEquals("expected sync1: " + sync1.toString() + " == sync 2" + syncs.get(0).toString(), sync1, syncs.get(0));
 
         syncs = engine.getPeriodicSyncs(account, 0, authority2);
         assertEquals(1, syncs.size());
@@ -451,6 +674,11 @@
     }
 
     @Override
+    public Resources getResources() {
+        return mRealContext.getResources();
+    }
+
+    @Override
     public File getFilesDir() {
         return mRealContext.getFilesDir();
     }