merged 231cc608d06ffc31c24bf8aa8c8275bdd2636581
diff --git a/core/java/android/content/ActiveSyncInfo.aidl b/core/java/android/content/ActiveSyncInfo.aidl
new file mode 100644
index 0000000..1142206
--- /dev/null
+++ b/core/java/android/content/ActiveSyncInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2009 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 ActiveSyncInfo;
diff --git a/core/java/android/content/ActiveSyncInfo.java b/core/java/android/content/ActiveSyncInfo.java
new file mode 100644
index 0000000..209dffa
--- /dev/null
+++ b/core/java/android/content/ActiveSyncInfo.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2009 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.Parcel;
+import android.os.Parcelable.Creator;
+
+/** @hide */
+public class ActiveSyncInfo {
+    public final int authorityId;
+    public final Account account;
+    public final String authority;
+    public final long startTime;
+    
+    ActiveSyncInfo(int authorityId, Account account, String authority,
+            long startTime) {
+        this.authorityId = authorityId;
+        this.account = account;
+        this.authority = authority;
+        this.startTime = startTime;
+    }
+    
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeInt(authorityId);
+        account.writeToParcel(parcel, 0);
+        parcel.writeString(authority);
+        parcel.writeLong(startTime);
+    }
+
+    ActiveSyncInfo(Parcel parcel) {
+        authorityId = parcel.readInt();
+        account = new Account(parcel);
+        authority = parcel.readString();
+        startTime = parcel.readLong();
+    }
+    
+    public static final Creator<ActiveSyncInfo> CREATOR = new Creator<ActiveSyncInfo>() {
+        public ActiveSyncInfo createFromParcel(Parcel in) {
+            return new ActiveSyncInfo(in);
+        }
+
+        public ActiveSyncInfo[] newArray(int size) {
+            return new ActiveSyncInfo[size];
+        }
+    };
+}
\ No newline at end of file
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 79cbc49..6f3b1b6 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -25,10 +25,14 @@
 import android.database.IContentObserver;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.text.TextUtils;
 import android.accounts.Account;
+import android.util.Config;
+import android.util.Log;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -86,8 +90,7 @@
      */
     public static final String CURSOR_DIR_BASE_TYPE = "vnd.android.cursor.dir";
     
-    public ContentResolver(Context context)
-    {
+    public ContentResolver(Context context) {
         mContext = context;
     }
 
@@ -646,7 +649,7 @@
             ContentObserver observer)
     {
         try {
-            ContentServiceNative.getDefault().registerContentObserver(uri, notifyForDescendents,
+            getContentService().registerContentObserver(uri, notifyForDescendents,
                     observer.getContentObserver());
         } catch (RemoteException e) {
         }
@@ -662,7 +665,7 @@
         try {
             IContentObserver contentObserver = observer.releaseContentObserver();
             if (contentObserver != null) {
-                ContentServiceNative.getDefault().unregisterContentObserver(
+                getContentService().unregisterContentObserver(
                         contentObserver);
             }
         } catch (RemoteException e) {
@@ -692,7 +695,7 @@
      */
     public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
         try {
-            ContentServiceNative.getDefault().notifyChange(
+            getContentService().notifyChange(
                     uri, observer == null ? null : observer.getContentObserver(),
                     observer != null && observer.deliverSelfNotifications(), syncToNetwork);
         } catch (RemoteException e) {
@@ -718,7 +721,7 @@
     public void startSync(Uri uri, Bundle extras) {
         validateSyncExtrasBundle(extras);
         try {
-            ContentServiceNative.getDefault().startSync(uri, extras);
+            getContentService().startSync(uri, extras);
         } catch (RemoteException e) {
         }
     }
@@ -761,7 +764,7 @@
 
     public void cancelSync(Uri uri) {
         try {
-            ContentServiceNative.getDefault().cancelSync(uri);
+            getContentService().cancelSync(uri);
         } catch (RemoteException e) {
         }
     }
@@ -822,6 +825,22 @@
         }
     }
 
+    /** @hide */
+    public static final String CONTENT_SERVICE_NAME = "content";
+    
+    /** @hide */
+    public static IContentService getContentService() {
+        if (sContentService != null) {
+            return sContentService;
+        }
+        IBinder b = ServiceManager.getService(CONTENT_SERVICE_NAME);
+        if (Config.LOGV) Log.v("ContentService", "default service binder = " + b);
+        sContentService = IContentService.Stub.asInterface(b);
+        if (Config.LOGV) Log.v("ContentService", "default service = " + sContentService);
+        return sContentService;
+    }
+    
+    private static IContentService sContentService;
     private final Context mContext;
     private static final String TAG = "ContentResolver";
 }
diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java
index b028868..c768ffa 100644
--- a/core/java/android/content/ContentService.java
+++ b/core/java/android/content/ContentService.java
@@ -16,11 +16,13 @@
 
 package android.content;
 
+import android.accounts.Account;
 import android.database.IContentObserver;
 import android.database.sqlite.SQLiteException;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.Parcel;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.Config;
@@ -34,7 +36,7 @@
 /**
  * {@hide}
  */
-public final class ContentService extends ContentServiceNative {
+public final class ContentService extends IContentService.Stub {
     private static final String TAG = "ContentService";
     private Context mContext;
     private boolean mFactoryTest;
@@ -73,6 +75,21 @@
         }
     }
 
+    @Override
+    public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+            throws RemoteException {
+        try {
+            return super.onTransact(code, data, reply, flags);
+        } catch (RuntimeException e) {
+            // The content service only throws security exceptions, so let's
+            // log all others.
+            if (!(e instanceof SecurityException)) {
+                Log.e(TAG, "Content Service Crash", e);
+            }
+            throw e;
+        }
+    }
+
     /*package*/ ContentService(Context context, boolean factoryTest) {
         mContext = context;
         mFactoryTest = factoryTest;
@@ -204,9 +221,158 @@
         }
     }
 
+    public boolean getSyncProviderAutomatically(String providerName) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
+                "no permission to read the sync settings");
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().getSyncProviderAutomatically(
+                        null, providerName);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return false;
+    }
+
+    public void setSyncProviderAutomatically(String providerName, boolean sync) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+                "no permission to write the sync settings");
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                syncManager.getSyncStorageEngine().setSyncProviderAutomatically(
+                        null, providerName, sync);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public boolean getListenForNetworkTickles() {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
+                "no permission to read the sync settings");
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().getListenForNetworkTickles();
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return false;
+    }
+    
+    public void setListenForNetworkTickles(boolean flag) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+                "no permission to write the sync settings");
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                syncManager.getSyncStorageEngine().setListenForNetworkTickles(flag);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public boolean isSyncActive(Account account, String authority) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
+                "no permission to read the sync stats");
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().isSyncActive(
+                        account, authority);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return false;
+    }
+    
+    public ActiveSyncInfo getActiveSync() {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
+                "no permission to read the sync stats");
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().getActiveSync();
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return null;
+    }
+    
+    public SyncStatusInfo getStatusByAuthority(String authority) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
+                "no permission to read the sync stats");
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().getStatusByAuthority(
+                        authority);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return null;
+    }
+    
+    public boolean isAuthorityPending(Account account, String authority) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
+                "no permission to read the sync stats");
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().isAuthorityPending(
+                        account, authority);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return false;
+    }
+    
+    public void addStatusChangeListener(int mask, ISyncStatusObserver callback) {
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                syncManager.getSyncStorageEngine().addStatusChangeListener(
+                        mask, callback);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+    
+    public void removeStatusChangeListener(ISyncStatusObserver callback) {
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                syncManager.getSyncStorageEngine().removeStatusChangeListener(
+                        callback);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+    
     public static IContentService main(Context context, boolean factoryTest) {
         ContentService service = new ContentService(context, factoryTest);
-        ServiceManager.addService("content", service);
+        ServiceManager.addService(ContentResolver.CONTENT_SERVICE_NAME, service);
         return service;
     }
 
diff --git a/core/java/android/content/ContentServiceNative.java b/core/java/android/content/ContentServiceNative.java
deleted file mode 100644
index 364f9ee..0000000
--- a/core/java/android/content/ContentServiceNative.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Copyright (C) 2006 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.database.IContentObserver;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.RemoteException;
-import android.os.IBinder;
-import android.os.Parcel;
-import android.os.ServiceManager;
-import android.os.Bundle;
-import android.util.Config;
-import android.util.Log;
-
-/**
- * {@hide}
- */
-abstract class ContentServiceNative extends Binder implements IContentService
-{
-    public ContentServiceNative()
-    {
-        attachInterface(this, descriptor);
-    }
-
-    /**
-     * Cast a Binder object into a content resolver interface, generating
-     * a proxy if needed.
-     */
-    static public IContentService asInterface(IBinder obj)
-    {
-        if (obj == null) {
-            return null;
-        }
-        IContentService in =
-            (IContentService)obj.queryLocalInterface(descriptor);
-        if (in != null) {
-            return in;
-        }
-
-        return new ContentServiceProxy(obj);
-    }
-
-    /**
-     * Retrieve the system's default/global content service.
-     */
-    static public IContentService getDefault()
-    {
-        if (gDefault != null) {
-            return gDefault;
-        }
-        IBinder b = ServiceManager.getService("content");
-        if (Config.LOGV) Log.v("ContentService", "default service binder = " + b);
-        gDefault = asInterface(b);
-        if (Config.LOGV) Log.v("ContentService", "default service = " + gDefault);
-        return gDefault;
-    }
-
-    @Override
-    public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
-    {
-        try {
-        switch (code) {
-            case 5038: {
-                data.readString(); // ignore the interface token that service generated
-                Uri uri = Uri.parse(data.readString());
-                notifyChange(uri, null, false, false);
-                return true;
-            }
-            
-            case REGISTER_CONTENT_OBSERVER_TRANSACTION: {
-                Uri uri = Uri.CREATOR.createFromParcel(data);
-                boolean notifyForDescendents = data.readInt() != 0;
-                IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder());
-                registerContentObserver(uri, notifyForDescendents, observer);
-                return true;
-            }
-
-            case UNREGISTER_CHANGE_OBSERVER_TRANSACTION: {
-                IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder());
-                unregisterContentObserver(observer);
-                return true;
-            }
-
-            case NOTIFY_CHANGE_TRANSACTION: {
-                Uri uri = Uri.CREATOR.createFromParcel(data);
-                IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder());
-                boolean observerWantsSelfNotifications = data.readInt() != 0;
-                boolean syncToNetwork = data.readInt() != 0;
-                notifyChange(uri, observer, observerWantsSelfNotifications, syncToNetwork);
-                return true;
-            }
-
-            case START_SYNC_TRANSACTION: {
-                Uri url = null;
-                int hasUrl = data.readInt();
-                if (hasUrl != 0) {
-                    url = Uri.CREATOR.createFromParcel(data);
-                }
-                startSync(url, data.readBundle());
-                return true;
-            }
-
-            case CANCEL_SYNC_TRANSACTION: {
-                Uri url = null;
-                int hasUrl = data.readInt();
-                if (hasUrl != 0) {
-                    url = Uri.CREATOR.createFromParcel(data);
-                }
-                cancelSync(url);
-                return true;
-            }
-
-            default:
-                return super.onTransact(code, data, reply, flags);
-        }
-        } catch (Exception e) {
-            Log.e("ContentServiceNative", "Caught exception in transact", e);
-        }
-
-        return false;
-    }
-
-    public IBinder asBinder()
-    {
-        return this;
-    }
-
-    private static IContentService gDefault;
-}
-
-
-final class ContentServiceProxy implements IContentService
-{
-    public ContentServiceProxy(IBinder remote)
-    {
-        mRemote = remote;
-    }
-
-    public IBinder asBinder()
-    {
-        return mRemote;
-    }
-
-    public void registerContentObserver(Uri uri, boolean notifyForDescendents,
-            IContentObserver observer) throws RemoteException
-    {
-        Parcel data = Parcel.obtain();
-        uri.writeToParcel(data, 0);
-        data.writeInt(notifyForDescendents ? 1 : 0);
-        data.writeStrongInterface(observer);
-        mRemote.transact(REGISTER_CONTENT_OBSERVER_TRANSACTION, data, null, 0);
-        data.recycle();
-    }
-
-    public void unregisterContentObserver(IContentObserver observer) throws RemoteException {
-        Parcel data = Parcel.obtain();
-        data.writeStrongInterface(observer);
-        mRemote.transact(UNREGISTER_CHANGE_OBSERVER_TRANSACTION, data, null, 0);
-        data.recycle();
-    }
-
-    public void notifyChange(Uri uri, IContentObserver observer,
-            boolean observerWantsSelfNotifications, boolean syncToNetwork)
-            throws RemoteException {
-        Parcel data = Parcel.obtain();
-        uri.writeToParcel(data, 0);
-        data.writeStrongInterface(observer);
-        data.writeInt(observerWantsSelfNotifications ? 1 : 0);
-        data.writeInt(syncToNetwork ? 1 : 0);
-        mRemote.transact(NOTIFY_CHANGE_TRANSACTION, data, null, IBinder.FLAG_ONEWAY);
-        data.recycle();
-    }
-
-    public void startSync(Uri url, Bundle extras) throws RemoteException {
-        Parcel data = Parcel.obtain();
-        if (url == null) {
-            data.writeInt(0);
-        } else {
-            data.writeInt(1);
-            url.writeToParcel(data, 0);
-        }
-        extras.writeToParcel(data, 0);
-        mRemote.transact(START_SYNC_TRANSACTION, data, null, IBinder.FLAG_ONEWAY);
-        data.recycle();
-    }
-
-    public void cancelSync(Uri url) throws RemoteException {
-        Parcel data = Parcel.obtain();
-        if (url == null) {
-            data.writeInt(0);
-        } else {
-            data.writeInt(1);
-            url.writeToParcel(data, 0);
-        }
-        mRemote.transact(CANCEL_SYNC_TRANSACTION, data, null /* reply */, IBinder.FLAG_ONEWAY);
-        data.recycle();
-    }
-
-    private IBinder mRemote;
-}
-
diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl
new file mode 100644
index 0000000..4352227
--- /dev/null
+++ b/core/java/android/content/IContentService.aidl
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2009 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.content.ActiveSyncInfo;
+import android.content.ISyncStatusObserver;
+import android.content.SyncStatusInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.database.IContentObserver;
+
+/**
+ * @hide
+ */
+interface IContentService {
+    void registerContentObserver(in Uri uri, boolean notifyForDescendentsn,
+            IContentObserver observer);
+    void unregisterContentObserver(IContentObserver observer);
+
+    void notifyChange(in Uri uri, IContentObserver observer,
+            boolean observerWantsSelfNotifications, boolean syncToNetwork);
+
+    void startSync(in Uri url, in Bundle extras);
+    void cancelSync(in Uri uri);
+    
+    /**
+     * Check if the provider should be synced when a network tickle is received
+     * @param providerName the provider whose setting we are querying
+     * @return true of the provider should be synced when a network tickle is received
+     */
+    boolean getSyncProviderAutomatically(String providerName);
+
+    /**
+     * Set whether or not the provider is synced when it receives a network tickle.
+     *
+     * @param providerName the provider whose behavior is being controlled
+     * @param sync true if the provider should be synced when tickles are received for it
+     */
+    void setSyncProviderAutomatically(String providerName, boolean sync);
+
+    void setListenForNetworkTickles(boolean flag);
+
+    boolean getListenForNetworkTickles();
+    
+    /**
+     * Returns true if there is currently a sync operation for the given
+     * account or authority in the pending list, or actively being processed.
+     */
+    boolean isSyncActive(in Account account, String authority);
+    
+    ActiveSyncInfo getActiveSync();
+    
+    /**
+     * Returns the status that matches the authority. If there are multiples accounts for
+     * the authority, the one with the latest "lastSuccessTime" status is returned.
+     * @param authority the authority whose row should be selected
+     * @return the SyncStatusInfo for the authority, or null if none exists
+     */
+    SyncStatusInfo getStatusByAuthority(String authority);
+
+    /**
+     * Return true if the pending status is true of any matching authorities.
+     */
+    boolean isAuthorityPending(in Account account, String authority);
+    
+    void addStatusChangeListener(int mask, ISyncStatusObserver callback);
+    
+    void removeStatusChangeListener(ISyncStatusObserver callback);
+}
diff --git a/core/java/android/content/IContentService.java b/core/java/android/content/IContentService.java
deleted file mode 100644
index a3047da..0000000
--- a/core/java/android/content/IContentService.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2006 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.database.IContentObserver;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.os.IBinder;
-import android.os.IInterface;
-import android.os.Bundle;
-
-/**
- * {@hide}
- */
-public interface IContentService extends IInterface
-{
-    public void registerContentObserver(Uri uri, boolean notifyForDescendentsn,
-            IContentObserver observer) throws RemoteException;
-    public void unregisterContentObserver(IContentObserver observer) throws RemoteException;
-
-    public void notifyChange(Uri uri, IContentObserver observer,
-            boolean observerWantsSelfNotifications, boolean syncToNetwork)
-            throws RemoteException;
-
-    public void startSync(Uri url, Bundle extras) throws RemoteException;
-    public void cancelSync(Uri uri) throws RemoteException;
-
-    static final String SERVICE_NAME = "content";
-
-    /* IPC constants */
-    static final String descriptor = "android.content.IContentService";
-
-    static final int REGISTER_CONTENT_OBSERVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1;
-    static final int UNREGISTER_CHANGE_OBSERVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2;
-    static final int NOTIFY_CHANGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3;
-    static final int START_SYNC_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4;
-    static final int CANCEL_SYNC_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5;
-}
-
diff --git a/core/java/android/content/ISyncStatusObserver.aidl b/core/java/android/content/ISyncStatusObserver.aidl
new file mode 100644
index 0000000..eb26845
--- /dev/null
+++ b/core/java/android/content/ISyncStatusObserver.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2009 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;
+
+/**
+ * @hide
+ */
+oneway interface ISyncStatusObserver {
+    void onStatusChanged(int which);
+}
diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java
index 9bf41c7..019abb8 100644
--- a/core/java/android/content/SyncManager.java
+++ b/core/java/android/content/SyncManager.java
@@ -44,7 +44,6 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
-import android.os.Parcel;
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
@@ -54,6 +53,7 @@
 import android.provider.Sync;
 import android.provider.Settings;
 import android.provider.Sync.History;
+import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.text.format.Time;
 import android.util.Config;
@@ -69,7 +69,9 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -78,7 +80,6 @@
 import java.util.Observer;
 import java.util.Observable;
 import java.util.Set;
-import java.util.HashSet;
 
 /**
  * @hide
@@ -134,7 +135,6 @@
     volatile private PowerManager.WakeLock mHandleAlarmWakeLock;
     volatile private boolean mDataConnectionIsConnected = false;
     volatile private boolean mStorageIsLow = false;
-    private Sync.Settings.QueryMap mSyncSettings;
 
     private final NotificationManager mNotificationMgr;
     private AlarmManager mAlarmService = null;
@@ -259,17 +259,6 @@
     private static final String SYNC_POLL_ALARM = "android.content.syncmanager.SYNC_POLL_ALARM";
     private final SyncHandler mSyncHandler;
 
-    private static final String[] SYNC_ACTIVE_PROJECTION = new String[]{
-            Sync.Active.ACCOUNT,
-            Sync.Active.AUTHORITY,
-            Sync.Active.START_TIME,
-    };
-
-    private static final String[] SYNC_PENDING_PROJECTION = new String[]{
-            Sync.Pending.ACCOUNT,
-            Sync.Pending.AUTHORITY
-    };
-
     private static final int MAX_SYNC_POLL_DELAY_SECONDS = 36 * 60 * 60; // 36 hours
     private static final int MIN_SYNC_POLL_DELAY_SECONDS = 24 * 60 * 60; // 24 hours
 
@@ -331,6 +320,14 @@
         mHandleAlarmWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                 HANDLE_SYNC_ALARM_WAKE_LOCK);
         mHandleAlarmWakeLock.setReferenceCounted(false);
+
+        mSyncStorageEngine.addStatusChangeListener(
+                SyncStorageEngine.CHANGE_SETTINGS, new ISyncStatusObserver.Stub() {
+            public void onStatusChanged(int which) {
+                // force the sync loop to run if the settings change
+                sendCheckAlarmsMessage();
+            }
+        });
     }
 
     private synchronized void initializeSyncPoll() {
@@ -455,20 +452,10 @@
         return mActiveSyncContext;
     }
 
-    private Sync.Settings.QueryMap getSyncSettings() {
-        if (mSyncSettings == null) {
-            mSyncSettings = new Sync.Settings.QueryMap(mContext.getContentResolver(), true,
-                    new Handler());
-            mSyncSettings.addObserver(new Observer(){
-                public void update(Observable o, Object arg) {
-                    // force the sync loop to run if the settings change
-                    sendCheckAlarmsMessage();
-                }
-            });
-        }
-        return mSyncSettings;
+    public SyncStorageEngine getSyncStorageEngine() {
+        return mSyncStorageEngine;
     }
-
+    
     private void ensureAlarmService() {
         if (mAlarmService == null) {
             mAlarmService = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
@@ -575,15 +562,15 @@
 
         int source;
         if (uploadOnly) {
-            source = Sync.History.SOURCE_LOCAL;
+            source = SyncStorageEngine.SOURCE_LOCAL;
         } else if (force) {
-            source = Sync.History.SOURCE_USER;
+            source = SyncStorageEngine.SOURCE_USER;
         } else if (url == null) {
-            source = Sync.History.SOURCE_POLL;
+            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 = Sync.History.SOURCE_SERVER;
+            source = SyncStorageEngine.SOURCE_SERVER;
         }
 
         // compile a list of authorities that have sync adapters
@@ -655,8 +642,7 @@
 
     public void updateHeartbeatTime() {
         mHeartbeatTime = SystemClock.elapsedRealtime();
-        mContext.getContentResolver().notifyChange(Sync.Active.CONTENT_URI,
-                null /* this change wasn't made through an observer */);
+        mSyncStorageEngine.reportActiveChange();
     }
 
     private void sendSyncAlarmMessage() {
@@ -862,7 +848,7 @@
         final String key;
         long earliestRunTime;
         long delay;
-        Long rowId = null;
+        SyncStorageEngine.PendingOperation pendingOperation;
 
         SyncOperation(Account account, int source, String authority, Bundle extras, long delay) {
             this.account = account;
@@ -902,7 +888,7 @@
             sb.append(" when: ").append(earliestRunTime);
             sb.append(" delay: ").append(delay);
             sb.append(" key: {").append(key).append("}");
-            if (rowId != null) sb.append(" rowId: ").append(rowId);
+            if (pendingOperation != null) sb.append(" pendingOperation: ").append(pendingOperation);
             return sb.toString();
         }
 
@@ -1014,249 +1000,276 @@
 
     protected void dump(FileDescriptor fd, PrintWriter pw) {
         StringBuilder sb = new StringBuilder();
-        dumpSyncState(sb);
-        sb.append("\n");
+        dumpSyncState(pw, sb);
         if (isSyncEnabled()) {
-            dumpSyncHistory(sb);
+            dumpSyncHistory(pw, sb);
         }
-        pw.println(sb.toString());
 
+        pw.println();
         pw.println("SyncAdapters:");
         for (RegisteredServicesCache.ServiceInfo info : mSyncAdapters.getAllServices()) {
             pw.println("  " + info);
         }
     }
 
-    protected void dumpSyncState(StringBuilder sb) {
-        sb.append("sync enabled: ").append(isSyncEnabled()).append("\n");
-        sb.append("data connected: ").append(mDataConnectionIsConnected).append("\n");
-        sb.append("memory low: ").append(mStorageIsLow).append("\n");
+    static String formatTime(long time) {
+        Time tobj = new Time();
+        tobj.set(time);
+        return tobj.format("%Y-%m-%d %H:%M:%S");
+    }
+    
+    protected void dumpSyncState(PrintWriter pw, StringBuilder sb) {
+        pw.print("sync enabled: "); pw.println(isSyncEnabled());
+        pw.print("data connected: "); pw.println(mDataConnectionIsConnected);
+        pw.print("memory low: "); pw.println(mStorageIsLow);
 
         final Account[] accounts = mAccounts;
-        sb.append("accounts: ");
+        pw.print("accounts: ");
         if (accounts != null) {
-            sb.append(accounts.length);
+            pw.println(accounts.length);
         } else {
-            sb.append("none");
+            pw.println("none");
         }
-        sb.append("\n");
         final long now = SystemClock.elapsedRealtime();
-        sb.append("now: ").append(now).append("\n");
-        sb.append("uptime: ").append(DateUtils.formatElapsedTime(now/1000)).append(" (HH:MM:SS)\n");
-        sb.append("time spent syncing : ")
-                .append(DateUtils.formatElapsedTime(
-                        mSyncHandler.mSyncTimeTracker.timeSpentSyncing() / 1000))
-                .append(" (HH:MM:SS), sync ")
-                .append(mSyncHandler.mSyncTimeTracker.mLastWasSyncing ? "" : "not ")
-                .append("in progress").append("\n");
+        pw.print("now: "); pw.println(now);
+        pw.print("uptime: "); pw.print(DateUtils.formatElapsedTime(now/1000));
+                pw.println(" (HH:MM:SS)");
+        pw.print("time spent syncing: ");
+                pw.print(DateUtils.formatElapsedTime(
+                        mSyncHandler.mSyncTimeTracker.timeSpentSyncing() / 1000));
+                pw.print(" (HH:MM:SS), sync ");
+                pw.print(mSyncHandler.mSyncTimeTracker.mLastWasSyncing ? "" : "not ");
+                pw.println("in progress");
         if (mSyncHandler.mAlarmScheduleTime != null) {
-            sb.append("next alarm time: ").append(mSyncHandler.mAlarmScheduleTime)
-                    .append(" (")
-                    .append(DateUtils.formatElapsedTime((mSyncHandler.mAlarmScheduleTime-now)/1000))
-                    .append(" (HH:MM:SS) from now)\n");
+            pw.print("next alarm time: "); pw.print(mSyncHandler.mAlarmScheduleTime);
+                    pw.print(" (");
+                    pw.print(DateUtils.formatElapsedTime((mSyncHandler.mAlarmScheduleTime-now)/1000));
+                    pw.println(" (HH:MM:SS) from now)");
         } else {
-            sb.append("no alarm is scheduled (there had better not be any pending syncs)\n");
+            pw.println("no alarm is scheduled (there had better not be any pending syncs)");
         }
 
-        sb.append("active sync: ").append(mActiveSyncContext).append("\n");
+        pw.print("active sync: "); pw.println(mActiveSyncContext);
 
-        sb.append("notification info: ");
+        pw.print("notification info: ");
+        sb.setLength(0);
         mSyncHandler.mSyncNotificationInfo.toString(sb);
-        sb.append("\n");
+        pw.println(sb.toString());
 
         synchronized (mSyncQueue) {
-            sb.append("sync queue: ");
+            pw.print("sync queue: ");
+            sb.setLength(0);
             mSyncQueue.dump(sb);
+            pw.println(sb.toString());
         }
 
-        Cursor c = mSyncStorageEngine.query(Sync.Active.CONTENT_URI,
-                SYNC_ACTIVE_PROJECTION, null, null, null);
-        sb.append("\n");
-        try {
-            if (c.moveToNext()) {
-                final long durationInSeconds = (now - c.getLong(2)) / 1000;
-                sb.append("Active sync: ").append(c.getString(0))
-                        .append(" ").append(c.getString(1))
-                        .append(", duration is ")
-                        .append(DateUtils.formatElapsedTime(durationInSeconds)).append(".\n");
-            } else {
-                sb.append("No sync is in progress.\n");
-            }
-        } finally {
-            c.close();
-        }
-
-        c = mSyncStorageEngine.query(Sync.Pending.CONTENT_URI,
-                SYNC_PENDING_PROJECTION, null, null, "account, authority");
-        sb.append("\nPending Syncs\n");
-        try {
-            if (c.getCount() != 0) {
-                dumpSyncPendingHeader(sb);
-                while (c.moveToNext()) {
-                    dumpSyncPendingRow(sb, c);
-                }
-                dumpSyncPendingFooter(sb);
-            } else {
-                sb.append("none\n");
-            }
-        } finally {
-            c.close();
-        }
-
-        Account currentAccount = null;
-        c = mSyncStorageEngine.query(Sync.Status.CONTENT_URI,
-                STATUS_PROJECTION, null, null, "account_type, account, authority");
-        sb.append("\nSync history by account and authority\n");
-        try {
-            while (c.moveToNext()) {
-                final Account account = new Account(c.getString(0), c.getString(13));
-                if (!account.equals(currentAccount)) {
-                    if (currentAccount != null) {
-                        dumpSyncHistoryFooter(sb);
-                    }
-                    currentAccount = account;
-                    dumpSyncHistoryHeader(sb, currentAccount);
-                }
-
-                dumpSyncHistoryRow(sb, c);
-            }
-            if (c.getCount() > 0) dumpSyncHistoryFooter(sb);
-        } finally {
-            c.close();
-        }
-    }
-
-    private void dumpSyncHistoryHeader(StringBuilder sb, Account account) {
-        sb.append(" ").append(account).append("\n");
-        sb.append("  ___________________________________________________________________________________________________________________________\n");
-        sb.append(" |                 |             num times synced           |   total  |         last success          |                     |\n");
-        sb.append(" | authority       | local |  poll | server |  user | total | duration |  source |               time  |   result if failing |\n");
-    }
-
-    private static String[] STATUS_PROJECTION = new String[]{
-            Sync.Status.ACCOUNT, // 0
-            Sync.Status.AUTHORITY, // 1
-            Sync.Status.NUM_SYNCS, // 2
-            Sync.Status.TOTAL_ELAPSED_TIME, // 3
-            Sync.Status.NUM_SOURCE_LOCAL, // 4
-            Sync.Status.NUM_SOURCE_POLL, // 5
-            Sync.Status.NUM_SOURCE_SERVER, // 6
-            Sync.Status.NUM_SOURCE_USER, // 7
-            Sync.Status.LAST_SUCCESS_SOURCE, // 8
-            Sync.Status.LAST_SUCCESS_TIME, // 9
-            Sync.Status.LAST_FAILURE_SOURCE, // 10
-            Sync.Status.LAST_FAILURE_TIME, // 11
-            Sync.Status.LAST_FAILURE_MESG, // 12
-            Sync.Status.ACCOUNT_TYPE, // 13
-    };
-
-    private void dumpSyncHistoryRow(StringBuilder sb, Cursor c) {
-        boolean hasSuccess = !c.isNull(9);
-        boolean hasFailure = !c.isNull(11);
-        Time timeSuccess = new Time();
-        if (hasSuccess) timeSuccess.set(c.getLong(9));
-        Time timeFailure = new Time();
-        if (hasFailure) timeFailure.set(c.getLong(11));
-        sb.append(String.format(" | %-15s | %5d | %5d | %6d | %5d | %5d | %8s | %7s | %19s | %19s |\n",
-                c.getString(1),
-                c.getLong(4),
-                c.getLong(5),
-                c.getLong(6),
-                c.getLong(7),
-                c.getLong(2),
-                DateUtils.formatElapsedTime(c.getLong(3)/1000),
-                hasSuccess ? Sync.History.SOURCES[c.getInt(8)] : "",
-                hasSuccess ? timeSuccess.format("%Y-%m-%d %H:%M:%S") : "",
-                hasFailure ? History.mesgToString(c.getString(12)) : ""));
-    }
-
-    private void dumpSyncHistoryFooter(StringBuilder sb) {
-        sb.append(" |___________________________________________________________________________________________________________________________|\n");
-    }
-
-    private void dumpSyncPendingHeader(StringBuilder sb) {
-        sb.append(" ____________________________________________________\n");
-        sb.append(" | account                        | authority       |\n");
-    }
-
-    private void dumpSyncPendingRow(StringBuilder sb, Cursor c) {
-        sb.append(String.format(" | %-30s | %-15s |\n", c.getString(0), c.getString(1)));
-    }
-
-    private void dumpSyncPendingFooter(StringBuilder sb) {
-        sb.append(" |__________________________________________________|\n");
-    }
-
-    protected void dumpSyncHistory(StringBuilder sb) {
-        Cursor c = mSyncStorageEngine.query(Sync.History.CONTENT_URI, null, "event=?",
-                new String[]{String.valueOf(Sync.History.EVENT_STOP)},
-                Sync.HistoryColumns.EVENT_TIME + " desc");
-        try {
-            long numSyncsLastHour = 0, durationLastHour = 0;
-            long numSyncsLastDay = 0, durationLastDay = 0;
-            long numSyncsLastWeek = 0, durationLastWeek = 0;
-            long numSyncsLast4Weeks = 0, durationLast4Weeks = 0;
-            long numSyncsTotal = 0, durationTotal = 0;
-
-            long now = System.currentTimeMillis();
-            int indexEventTime = c.getColumnIndexOrThrow(Sync.History.EVENT_TIME);
-            int indexElapsedTime = c.getColumnIndexOrThrow(Sync.History.ELAPSED_TIME);
-            while (c.moveToNext()) {
-                long duration = c.getLong(indexElapsedTime);
-                long endTime = c.getLong(indexEventTime) + duration;
-                long millisSinceStart = now - endTime;
-                numSyncsTotal++;
-                durationTotal += duration;
-                if (millisSinceStart < MILLIS_IN_HOUR) {
-                    numSyncsLastHour++;
-                    durationLastHour += duration;
-                }
-                if (millisSinceStart < MILLIS_IN_DAY) {
-                    numSyncsLastDay++;
-                    durationLastDay += duration;
-                }
-                if (millisSinceStart < MILLIS_IN_WEEK) {
-                    numSyncsLastWeek++;
-                    durationLastWeek += duration;
-                }
-                if (millisSinceStart < MILLIS_IN_4WEEKS) {
-                    numSyncsLast4Weeks++;
-                    durationLast4Weeks += duration;
-                }
-            }
-            dumpSyncIntervalHeader(sb);
-            dumpSyncInterval(sb, "hour", MILLIS_IN_HOUR, numSyncsLastHour, durationLastHour);
-            dumpSyncInterval(sb, "day", MILLIS_IN_DAY, numSyncsLastDay, durationLastDay);
-            dumpSyncInterval(sb, "week", MILLIS_IN_WEEK, numSyncsLastWeek, durationLastWeek);
-            dumpSyncInterval(sb, "4 weeks",
-                    MILLIS_IN_4WEEKS, numSyncsLast4Weeks, durationLast4Weeks);
-            dumpSyncInterval(sb, "total", 0, numSyncsTotal, durationTotal);
-            dumpSyncIntervalFooter(sb);
-        } finally {
-            c.close();
-        }
-    }
-
-    private void dumpSyncIntervalHeader(StringBuilder sb) {
-        sb.append("Sync Stats\n");
-        sb.append(" ___________________________________________________________\n");
-        sb.append(" |          |        |   duration in sec   |               |\n");
-        sb.append(" | interval |  count |  average |    total | % of interval |\n");
-    }
-
-    private void dumpSyncInterval(StringBuilder sb, String label,
-            long interval, long numSyncs, long duration) {
-        sb.append(String.format(" | %-8s | %6d | %8.1f | %8.1f",
-                label, numSyncs, ((float)duration/numSyncs)/1000, (float)duration/1000));
-        if (interval > 0) {
-            sb.append(String.format(" | %13.2f |\n", ((float)duration/interval)*100.0));
+        ActiveSyncInfo active = mSyncStorageEngine.getActiveSync();
+        if (active != null) {
+            SyncStorageEngine.AuthorityInfo authority
+                    = mSyncStorageEngine.getAuthority(active.authorityId);
+            final long durationInSeconds = (now - active.startTime) / 1000;
+            pw.print("Active sync: ");
+                    pw.print(authority != null ? authority.account : "<no account>");
+                    pw.print(" ");
+                    pw.print(authority != null ? authority.authority : "<no account>");
+                    pw.print(", duration is ");
+                    pw.println(DateUtils.formatElapsedTime(durationInSeconds));
         } else {
-            sb.append(String.format(" | %13s |\n", "na"));
+            pw.println("No sync is in progress.");
+        }
+
+        ArrayList<SyncStorageEngine.PendingOperation> ops
+                = mSyncStorageEngine.getPendingOperations();
+        if (ops != null && ops.size() > 0) {
+            pw.println();
+            pw.println("Pending Syncs");
+            final int N = ops.size();
+            for (int i=0; i<N; i++) {
+                SyncStorageEngine.PendingOperation op = ops.get(i);
+                pw.print("  #"); pw.print(i); pw.print(": account=");
+                pw.print(op.account.mName); pw.print(":");
+                pw.print(op.account.mType); pw.print(" authority=");
+                pw.println(op.authority);
+                if (op.extras != null && op.extras.size() > 0) {
+                    sb.setLength(0);
+                    SyncOperation.extrasToStringBuilder(op.extras, sb);
+                    pw.print("    extras: "); pw.println(sb.toString());
+                }
+            }
+        }
+
+        HashSet<Account> processedAccounts = new HashSet<Account>();
+        ArrayList<SyncStatusInfo> statuses
+                = mSyncStorageEngine.getSyncStatus();
+        if (statuses != null && statuses.size() > 0) {
+            pw.println();
+            pw.println("Sync Status");
+            final int N = statuses.size();
+            for (int i=0; i<N; i++) {
+                SyncStatusInfo status = statuses.get(i);
+                SyncStorageEngine.AuthorityInfo authority
+                        = mSyncStorageEngine.getAuthority(status.authorityId);
+                if (authority != null) {
+                    Account curAccount = authority.account;
+                    
+                    if (processedAccounts.contains(curAccount)) {
+                        continue;
+                    }
+                    
+                    processedAccounts.add(curAccount);
+                    
+                    pw.print("  Account "); pw.print(authority.account.mName);
+                            pw.print(" "); pw.print(authority.account.mType);
+                            pw.println(":");
+                    for (int j=i; j<N; j++) {
+                        status = statuses.get(j);
+                        authority = mSyncStorageEngine.getAuthority(status.authorityId);
+                        if (!curAccount.equals(authority.account)) {
+                            continue;
+                        }
+                        pw.print("    "); pw.print(authority.authority);
+                        pw.println(":");
+                        pw.print("      count: local="); pw.print(status.numSourceLocal);
+                                pw.print(" poll="); pw.print(status.numSourcePoll);
+                                pw.print(" server="); pw.print(status.numSourceServer);
+                                pw.print(" user="); pw.print(status.numSourceUser);
+                                pw.print(" total="); pw.println(status.numSyncs);
+                        pw.print("      total duration: ");
+                                pw.println(DateUtils.formatElapsedTime(
+                                        status.totalElapsedTime/1000));
+                        if (status.lastSuccessTime != 0) {
+                            pw.print("      SUCCESS: source=");
+                                    pw.print(SyncStorageEngine.SOURCES[
+                                            status.lastSuccessSource]);
+                                    pw.print(" time=");
+                                    pw.println(formatTime(status.lastSuccessTime));
+                        } else {
+                            pw.print("      FAILURE: source=");
+                                    pw.print(SyncStorageEngine.SOURCES[
+                                            status.lastFailureSource]);
+                                    pw.print(" initialTime=");
+                                    pw.print(formatTime(status.initialFailureTime));
+                                    pw.print(" lastTime=");
+                                    pw.println(formatTime(status.lastFailureTime));
+                            pw.print("      message: "); pw.println(status.lastFailureMesg);
+                        }
+                    }
+                }
+            }
         }
     }
 
-    private void dumpSyncIntervalFooter(StringBuilder sb) {
-        sb.append(" |_________________________________________________________|\n");
+    private void dumpTimeSec(PrintWriter pw, long time) {
+        pw.print(time/1000); pw.print('.'); pw.print((time/100)%10);
+        pw.print('s');
+    }
+    
+    private void dumpDayStatistic(PrintWriter pw, SyncStorageEngine.DayStats ds) {
+        pw.print("Success ("); pw.print(ds.successCount);
+        if (ds.successCount > 0) {
+            pw.print(" for "); dumpTimeSec(pw, ds.successTime);
+            pw.print(" avg="); dumpTimeSec(pw, ds.successTime/ds.successCount);
+        }
+        pw.print(") Failure ("); pw.print(ds.failureCount);
+        if (ds.failureCount > 0) {
+            pw.print(" for "); dumpTimeSec(pw, ds.failureTime);
+            pw.print(" avg="); dumpTimeSec(pw, ds.failureTime/ds.failureCount);
+        }
+        pw.println(")");
+    }
+    
+    protected void dumpSyncHistory(PrintWriter pw, StringBuilder sb) {
+        SyncStorageEngine.DayStats dses[] = mSyncStorageEngine.getDayStatistics();
+        if (dses != null && dses[0] != null) {
+            pw.println();
+            pw.println("Sync Statistics");
+            pw.print("  Today:  "); dumpDayStatistic(pw, dses[0]);
+            int today = dses[0].day;
+            int i;
+            SyncStorageEngine.DayStats ds;
+            
+            // Print each day in the current week.
+            for (i=1; i<=6 && i < dses.length; i++) {
+                ds = dses[i];
+                if (ds == null) break;
+                int delta = today-ds.day;
+                if (delta > 6) break;
+                
+                pw.print("  Day-"); pw.print(delta); pw.print(":  ");
+                dumpDayStatistic(pw, ds);
+            }
+            
+            // Aggregate all following days into weeks and print totals.
+            int weekDay = today;
+            while (i < dses.length) {
+                SyncStorageEngine.DayStats aggr = null;
+                weekDay -= 7;
+                while (i < dses.length) {
+                    ds = dses[i];
+                    if (ds == null) {
+                        i = dses.length;
+                        break;
+                    }
+                    int delta = weekDay-ds.day;
+                    if (delta > 6) break;
+                    i++;
+                    
+                    if (aggr == null) {
+                        aggr = new SyncStorageEngine.DayStats(weekDay);
+                    }
+                    aggr.successCount += ds.successCount;
+                    aggr.successTime += ds.successTime;
+                    aggr.failureCount += ds.failureCount;
+                    aggr.failureTime += ds.failureTime;
+                }
+                if (aggr != null) {
+                    pw.print("  Week-"); pw.print((today-weekDay)/7); pw.print(": ");
+                    dumpDayStatistic(pw, aggr);
+                }
+            }
+        }
+        
+        ArrayList<SyncStorageEngine.SyncHistoryItem> items
+                = mSyncStorageEngine.getSyncHistory();
+        if (items != null && items.size() > 0) {
+            pw.println();
+            pw.println("Recent Sync History");
+            final int N = items.size();
+            for (int i=0; i<N; i++) {
+                SyncStorageEngine.SyncHistoryItem item = items.get(i);
+                SyncStorageEngine.AuthorityInfo authority
+                        = mSyncStorageEngine.getAuthority(item.authorityId);
+                pw.print("  #"); pw.print(i+1); pw.print(": ");
+                        if (authority != null) {
+                            pw.print(authority.account.mName);
+                            pw.print(":");
+                            pw.print(authority.account.mType);
+                            pw.print(" ");
+                            pw.print(authority.authority);
+                        } else {
+                            pw.print("<no account>");
+                        }
+                Time time = new Time();
+                time.set(item.eventTime);
+                pw.print(" "); pw.print(SyncStorageEngine.SOURCES[item.source]);
+                        pw.print(" @ ");
+                        pw.print(formatTime(item.eventTime));
+                        pw.print(" for ");
+                        dumpTimeSec(pw, item.elapsedTime);
+                        pw.println();
+                if (item.event != SyncStorageEngine.EVENT_STOP
+                        || item.upstreamActivity !=0
+                        || item.downstreamActivity != 0) {
+                    pw.print("    event="); pw.print(item.event);
+                            pw.print(" upstreamActivity="); pw.print(item.upstreamActivity);
+                            pw.print(" downstreamActivity="); pw.println(item.downstreamActivity);
+                }
+                if (item.mesg != null
+                        && !SyncStorageEngine.MESG_SUCCESS.equals(item.mesg)) {
+                    pw.print("    mesg="); pw.println(item.mesg);
+                }
+            }
+        }
     }
 
     /**
@@ -1541,7 +1554,6 @@
             // found that is runnable (not disabled, etc). If that one is ready to run then
             // start it, otherwise just get out.
             SyncOperation syncOperation;
-            final Sync.Settings.QueryMap syncSettings = getSyncSettings();
             final ConnectivityManager connManager = (ConnectivityManager)
                     mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
             final boolean backgroundDataSetting = connManager.getBackgroundDataSetting();
@@ -1568,9 +1580,9 @@
                     final boolean force = syncOperation.extras.getBoolean(
                             ContentResolver.SYNC_EXTRAS_FORCE, false);
                     if (!force && (!backgroundDataSetting
-                            || !syncSettings.getListenForNetworkTickles()
-                            || !syncSettings.getSyncProviderAutomatically(
-                                    syncOperation.authority))) {
+                            || !mSyncStorageEngine.getListenForNetworkTickles()
+                            || !mSyncStorageEngine.getSyncProviderAutomatically(
+                                    null, syncOperation.authority))) {
                         if (isLoggable) {
                             Log.v(TAG, "runStateIdle: sync off, dropping " + syncOperation);
                         }
@@ -1697,7 +1709,7 @@
                     if (isLoggable) {
                         Log.v(TAG, "finished sync operation " + syncOperation);
                     }
-                    historyMessage = History.MESG_SUCCESS;
+                    historyMessage = SyncStorageEngine.MESG_SUCCESS;
                     // TODO: set these correctly when the SyncResult is extended to include it
                     downstreamActivity = 0;
                     upstreamActivity = 0;
@@ -1723,7 +1735,7 @@
                         // we don't need to retry this in this case
                     }
                 }
-                historyMessage = History.MESG_CANCELED;
+                historyMessage = SyncStorageEngine.MESG_CANCELED;
                 downstreamActivity = 0;
                 upstreamActivity = 0;
             }
@@ -1758,14 +1770,22 @@
          *   If SyncResult.error() is true then it is safe to call this.
          */
         private int syncResultToErrorNumber(SyncResult syncResult) {
-            if (syncResult.syncAlreadyInProgress) return History.ERROR_SYNC_ALREADY_IN_PROGRESS;
-            if (syncResult.stats.numAuthExceptions > 0) return History.ERROR_AUTHENTICATION;
-            if (syncResult.stats.numIoExceptions > 0) return History.ERROR_IO;
-            if (syncResult.stats.numParseExceptions > 0) return History.ERROR_PARSE;
-            if (syncResult.stats.numConflictDetectedExceptions > 0) return History.ERROR_CONFLICT;
-            if (syncResult.tooManyDeletions) return History.ERROR_TOO_MANY_DELETIONS;
-            if (syncResult.tooManyRetries) return History.ERROR_TOO_MANY_RETRIES;
-            if (syncResult.databaseError) return History.ERROR_INTERNAL;
+            if (syncResult.syncAlreadyInProgress)
+                return SyncStorageEngine.ERROR_SYNC_ALREADY_IN_PROGRESS;
+            if (syncResult.stats.numAuthExceptions > 0)
+                return SyncStorageEngine.ERROR_AUTHENTICATION;
+            if (syncResult.stats.numIoExceptions > 0)
+                return SyncStorageEngine.ERROR_IO;
+            if (syncResult.stats.numParseExceptions > 0)
+                return SyncStorageEngine.ERROR_PARSE;
+            if (syncResult.stats.numConflictDetectedExceptions > 0)
+                return SyncStorageEngine.ERROR_CONFLICT;
+            if (syncResult.tooManyDeletions)
+                return SyncStorageEngine.ERROR_TOO_MANY_DELETIONS;
+            if (syncResult.tooManyRetries)
+                return SyncStorageEngine.ERROR_TOO_MANY_RETRIES;
+            if (syncResult.databaseError)
+                return SyncStorageEngine.ERROR_INTERNAL;
             throw new IllegalStateException("we are not in an error state, " + syncResult);
         }
 
@@ -1987,7 +2007,8 @@
             final int source = syncOperation.syncSource;
             final long now = System.currentTimeMillis();
 
-            EventLog.writeEvent(2720, syncOperation.authority, Sync.History.EVENT_START, source);
+            EventLog.writeEvent(2720, syncOperation.authority,
+                    SyncStorageEngine.EVENT_START, source);
 
             return mSyncStorageEngine.insertStartSyncEvent(
                     syncOperation.account, syncOperation.authority, now, source);
@@ -1995,7 +2016,8 @@
 
         public void stopSyncEvent(long rowId, SyncOperation syncOperation, String resultMessage,
                 int upstreamActivity, int downstreamActivity, long elapsedTime) {
-            EventLog.writeEvent(2720, syncOperation.authority, Sync.History.EVENT_STOP, syncOperation.syncSource);
+            EventLog.writeEvent(2720, syncOperation.authority,
+                    SyncStorageEngine.EVENT_STOP, syncOperation.syncSource);
 
             mSyncStorageEngine.stopSyncEvent(rowId, elapsedTime, resultMessage,
                     downstreamActivity, upstreamActivity);
@@ -2004,20 +2026,6 @@
 
     static class SyncQueue {
         private SyncStorageEngine mSyncStorageEngine;
-        private final String[] COLUMNS = new String[]{
-                "_id",
-                "authority",
-                "account",
-                "account_type",
-                "extras",
-                "source",
-        };
-        private static final int COLUMN_ID = 0;
-        private static final int COLUMN_AUTHORITY = 1;
-        private static final int COLUMN_ACCOUNT = 2;
-        private static final int COLUMN_ACCOUNT_TYPE = 3;
-        private static final int COLUMN_EXTRAS = 4;
-        private static final int COLUMN_SOURCE = 5;
 
         private static final boolean DEBUG_CHECK_DATA_CONSISTENCY = false;
 
@@ -2031,25 +2039,28 @@
 
         public SyncQueue(SyncStorageEngine syncStorageEngine) {
             mSyncStorageEngine = syncStorageEngine;
-            Cursor cursor = mSyncStorageEngine.getPendingSyncsCursor(COLUMNS);
-            try {
-                while (cursor.moveToNext()) {
-                    add(cursorToOperation(cursor),
-                            true /* this is being added from the database */);
-                }
-            } finally {
-                cursor.close();
-                if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
+            ArrayList<SyncStorageEngine.PendingOperation> ops
+                    = mSyncStorageEngine.getPendingOperations();
+            final int N = ops.size();
+            for (int i=0; i<N; i++) {
+                SyncStorageEngine.PendingOperation op = ops.get(i);
+                SyncOperation syncOperation = new SyncOperation(
+                        op.account, op.syncSource, op.authority, op.extras, 0);
+                syncOperation.pendingOperation = op;
+                add(syncOperation, op);
             }
+            
+            if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
         }
 
         public boolean add(SyncOperation operation) {
             return add(new SyncOperation(operation),
-                    false /* this is not coming from the database */);
+                    null /* this is not coming from the database */);
         }
 
-        private boolean add(SyncOperation operation, boolean fromDatabase) {
-            if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase);
+        private boolean add(SyncOperation operation,
+                SyncStorageEngine.PendingOperation pop) {
+            if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(pop == null);
 
             // If this operation is expedited then set its earliestRunTime to be immediately
             // before the head of the list, or not if none are in the list.
@@ -2081,7 +2092,7 @@
 
             if (existingOperation != null
                     && operation.earliestRunTime >= existingOperation.earliestRunTime) {
-                if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase);
+                if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(pop == null);
                 return false;
             }
 
@@ -2089,27 +2100,17 @@
                 removeByKey(operationKey);
             }
 
-            if (operation.rowId == null) {
-                byte[] extrasData = null;
-                Parcel parcel = Parcel.obtain();
-                try {
-                    operation.extras.writeToParcel(parcel, 0);
-                    extrasData = parcel.marshall();
-                } finally {
-                    parcel.recycle();
-                }
-                ContentValues values = new ContentValues();
-                values.put("account", operation.account.mName);
-                values.put("account_type", operation.account.mType);
-                values.put("authority", operation.authority);
-                values.put("source", operation.syncSource);
-                values.put("extras", extrasData);
-                Uri pendingUri = mSyncStorageEngine.insertIntoPending(values);
-                operation.rowId = pendingUri == null ? null : ContentUris.parseId(pendingUri);
-                if (operation.rowId == null) {
+            operation.pendingOperation = pop;
+            if (operation.pendingOperation == null) {
+                pop = new SyncStorageEngine.PendingOperation(
+                                operation.account, operation.syncSource,
+                                operation.authority, operation.extras);
+                pop = mSyncStorageEngine.insertIntoPending(pop);
+                if (pop == null) {
                     throw new IllegalStateException("error adding pending sync operation "
                             + operation);
                 }
+                operation.pendingOperation = pop;
             }
 
             if (DEBUG_CHECK_DATA_CONSISTENCY) {
@@ -2119,7 +2120,7 @@
             }
             mOpsByKey.put(operationKey, operation);
             mOpsByWhen.add(operation);
-            if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase);
+            if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(pop == null);
             return true;
         }
 
@@ -2131,7 +2132,7 @@
                         "unable to find " + operationToRemove + " in mOpsByWhen");
             }
 
-            if (mSyncStorageEngine.deleteFromPending(operationToRemove.rowId) != 1) {
+            if (!mSyncStorageEngine.deleteFromPending(operationToRemove.pendingOperation)) {
                 throw new IllegalStateException("unable to find pending row for "
                         + operationToRemove);
             }
@@ -2151,7 +2152,7 @@
                 throw new IllegalStateException("unable to find " + operation + " in mOpsByKey");
             }
 
-            if (mSyncStorageEngine.deleteFromPending(operation.rowId) != 1) {
+            if (!mSyncStorageEngine.deleteFromPending(operation.pendingOperation)) {
                 throw new IllegalStateException("unable to find pending row for " + operation);
             }
 
@@ -2173,7 +2174,7 @@
                             "unable to find " + syncOperation + " in mOpsByWhen");
                 }
 
-                if (mSyncStorageEngine.deleteFromPending(syncOperation.rowId) != 1) {
+                if (!mSyncStorageEngine.deleteFromPending(syncOperation.pendingOperation)) {
                     throw new IllegalStateException("unable to find pending row for "
                             + syncOperation);
                 }
@@ -2214,49 +2215,29 @@
             }
 
             if (checkDatabase) {
-                // check that the DB contains the same rows as the in-memory data structures
-                Cursor cursor = mSyncStorageEngine.getPendingSyncsCursor(COLUMNS);
-                try {
-                    if (mOpsByKey.size() != cursor.getCount()) {
-                        StringBuilder sb = new StringBuilder();
-                        DatabaseUtils.dumpCursor(cursor, sb);
+                final int N = mSyncStorageEngine.getPendingOperationCount();
+                if (mOpsByKey.size() != N) {
+                    ArrayList<SyncStorageEngine.PendingOperation> ops
+                            = mSyncStorageEngine.getPendingOperations();
+                    StringBuilder sb = new StringBuilder();
+                    for (int i=0; i<N; i++) {
+                        SyncStorageEngine.PendingOperation op = ops.get(i);
+                        sb.append("#");
+                        sb.append(i);
+                        sb.append(": account=");
+                        sb.append(op.account);
+                        sb.append(" syncSource=");
+                        sb.append(op.syncSource);
+                        sb.append(" authority=");
+                        sb.append(op.authority);
                         sb.append("\n");
-                        dump(sb);
-                        throw new IllegalStateException("DB size mismatch: "
-                                + mOpsByKey .size() + " != " + cursor.getCount() + "\n"
-                                + sb.toString());
                     }
-                } finally {
-                    cursor.close();
+                    dump(sb);
+                    throw new IllegalStateException("DB size mismatch: "
+                            + mOpsByKey.size() + " != " + N + "\n"
+                            + sb.toString());
                 }
             }
         }
-
-        private SyncOperation cursorToOperation(Cursor cursor) {
-            byte[] extrasData = cursor.getBlob(COLUMN_EXTRAS);
-            Bundle extras;
-            Parcel parcel = Parcel.obtain();
-            try {
-                parcel.unmarshall(extrasData, 0, extrasData.length);
-                parcel.setDataPosition(0);
-                extras = parcel.readBundle();
-            } catch (RuntimeException e) {
-                // A RuntimeException is thrown if we were unable to parse the parcel.
-                // Create an empty parcel in this case.
-                extras = new Bundle();
-            } finally {
-                parcel.recycle();
-            }
-
-            SyncOperation syncOperation = new SyncOperation(
-                    new Account(cursor.getString(COLUMN_ACCOUNT),
-                            cursor.getString(COLUMN_ACCOUNT_TYPE)),
-                    cursor.getInt(COLUMN_SOURCE),
-                    cursor.getString(COLUMN_AUTHORITY),
-                    extras,
-                    0 /* delay */);
-            syncOperation.rowId = cursor.getLong(COLUMN_ID);
-            return syncOperation;
-        }
     }
 }
diff --git a/core/java/android/content/SyncProvider.java b/core/java/android/content/SyncProvider.java
deleted file mode 100644
index 6ddd046..0000000
--- a/core/java/android/content/SyncProvider.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright 2007 The Android Open Source Project
-package android.content;
-
-import android.database.Cursor;
-import android.net.Uri;
-
-/**
- * ContentProvider that tracks the sync data and overall sync
- * history on the device.
- * 
- * @hide
- */
-public class SyncProvider extends ContentProvider {
-    public SyncProvider() {
-    }
-
-    private SyncStorageEngine mSyncStorageEngine;
-
-    @Override
-    public boolean onCreate() {
-        mSyncStorageEngine = SyncStorageEngine.getSingleton();
-        return true;
-    }
-
-    @Override
-    public Cursor query(Uri url, String[] projectionIn,
-                        String selection, String[] selectionArgs, String sort) {
-        return mSyncStorageEngine.query(url, projectionIn, selection, selectionArgs, sort);
-    }
-
-    @Override
-    public Uri insert(Uri url, ContentValues initialValues) {
-        return mSyncStorageEngine.insert(true /* the caller is the provider */,
-                url, initialValues);
-    }
-
-    @Override
-    public int delete(Uri url, String where, String[] whereArgs) {
-        return mSyncStorageEngine.delete(true /* the caller is the provider */,
-                url, where, whereArgs);
-    }
-
-    @Override
-    public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) {
-        return mSyncStorageEngine.update(true /* the caller is the provider */, 
-                url, initialValues, where, whereArgs);
-    }
-
-    @Override
-    public String getType(Uri url) {
-        return mSyncStorageEngine.getType(url);
-    }
-}
diff --git a/core/java/android/content/SyncStatusInfo.aidl b/core/java/android/content/SyncStatusInfo.aidl
new file mode 100644
index 0000000..b188c0b
--- /dev/null
+++ b/core/java/android/content/SyncStatusInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2009 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 SyncStatusInfo;
diff --git a/core/java/android/content/SyncStatusInfo.java b/core/java/android/content/SyncStatusInfo.java
new file mode 100644
index 0000000..6687fcb
--- /dev/null
+++ b/core/java/android/content/SyncStatusInfo.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2009 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.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+/** @hide */
+public class SyncStatusInfo implements Parcelable {
+    static final int VERSION = 1;
+    
+    public final int authorityId;
+    public long totalElapsedTime;
+    public int numSyncs;
+    public int numSourcePoll;
+    public int numSourceServer;
+    public int numSourceLocal;
+    public int numSourceUser;
+    public long lastSuccessTime;
+    public int lastSuccessSource;
+    public long lastFailureTime;
+    public int lastFailureSource;
+    public String lastFailureMesg;
+    public long initialFailureTime;
+    public boolean pending;
+    
+    SyncStatusInfo(int authorityId) {
+        this.authorityId = authorityId;
+    }
+
+    public int getLastFailureMesgAsInt(int def) {
+        try {
+            if (lastFailureMesg != null) {
+                return Integer.parseInt(lastFailureMesg);
+            }
+        } catch (NumberFormatException e) {
+        }
+        return def;
+    }
+    
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeInt(VERSION);
+        parcel.writeInt(authorityId);
+        parcel.writeLong(totalElapsedTime);
+        parcel.writeInt(numSyncs);
+        parcel.writeInt(numSourcePoll);
+        parcel.writeInt(numSourceServer);
+        parcel.writeInt(numSourceLocal);
+        parcel.writeInt(numSourceUser);
+        parcel.writeLong(lastSuccessTime);
+        parcel.writeInt(lastSuccessSource);
+        parcel.writeLong(lastFailureTime);
+        parcel.writeInt(lastFailureSource);
+        parcel.writeString(lastFailureMesg);
+        parcel.writeLong(initialFailureTime);
+        parcel.writeInt(pending ? 1 : 0);
+    }
+
+    SyncStatusInfo(Parcel parcel) {
+        int version = parcel.readInt();
+        if (version != VERSION) {
+            Log.w("SyncStatusInfo", "Unknown version: " + version);
+        }
+        authorityId = parcel.readInt();
+        totalElapsedTime = parcel.readLong();
+        numSyncs = parcel.readInt();
+        numSourcePoll = parcel.readInt();
+        numSourceServer = parcel.readInt();
+        numSourceLocal = parcel.readInt();
+        numSourceUser = parcel.readInt();
+        lastSuccessTime = parcel.readLong();
+        lastSuccessSource = parcel.readInt();
+        lastFailureTime = parcel.readLong();
+        lastFailureSource = parcel.readInt();
+        lastFailureMesg = parcel.readString();
+        initialFailureTime = parcel.readLong();
+        pending = parcel.readInt() != 0;
+    }
+    
+    public static final Creator<SyncStatusInfo> CREATOR = new Creator<SyncStatusInfo>() {
+        public SyncStatusInfo createFromParcel(Parcel in) {
+            return new SyncStatusInfo(in);
+        }
+
+        public SyncStatusInfo[] newArray(int size) {
+            return new SyncStatusInfo[size];
+        }
+    };
+}
\ No newline at end of file
diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java
index 2ad44d2..64b626d 100644
--- a/core/java/android/content/SyncStorageEngine.java
+++ b/core/java/android/content/SyncStorageEngine.java
@@ -1,132 +1,285 @@
+/*
+ * Copyright (C) 2009 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.Manifest;
+import com.android.internal.os.AtomicFile;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FastXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
 import android.accounts.Account;
 import android.database.Cursor;
-import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteException;
 import android.database.sqlite.SQLiteQueryBuilder;
-import android.net.Uri;
-import android.provider.Sync;
-import android.text.TextUtils;
-import android.util.Config;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
 import android.util.Log;
+import android.util.SparseArray;
+import android.util.Xml;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.util.ArrayList;
+import java.util.Calendar;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.Iterator;
+import java.util.TimeZone;
 
 import com.google.android.collect.Sets;
 
 /**
- * ContentProvider that tracks the sync data and overall sync
+ * Singleton that tracks the sync data and overall sync
  * history on the device.
  * 
  * @hide
  */
-public class SyncStorageEngine {
+public class SyncStorageEngine extends Handler {
     private static final String TAG = "SyncManager";
+    private static final boolean DEBUG = false;
+    private static final boolean DEBUG_FILE = false;
+    
+    // @VisibleForTesting
+    static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
 
-    private static final String DATABASE_NAME = "syncmanager.db";
-    private static final int DATABASE_VERSION = 11;
+    /** Enum value for a sync start event. */
+    public static final int EVENT_START = 0;
 
-    private static final int STATS = 1;
-    private static final int STATS_ID = 2;
-    private static final int HISTORY = 3;
-    private static final int HISTORY_ID = 4;
-    private static final int SETTINGS = 5;
-    private static final int PENDING = 7;
-    private static final int ACTIVE = 8;
-    private static final int STATUS = 9;
+    /** Enum value for a sync stop event. */
+    public static final int EVENT_STOP = 1;
 
-    private static final UriMatcher sURLMatcher =
-            new UriMatcher(UriMatcher.NO_MATCH);
+    // TODO: i18n -- grab these out of resources.
+    /** String names for the sync event types. */
+    public static final String[] EVENTS = { "START", "STOP" };
 
-    private static final HashMap<String,String> HISTORY_PROJECTION_MAP;
-    private static final HashMap<String,String> PENDING_PROJECTION_MAP;
-    private static final HashMap<String,String> ACTIVE_PROJECTION_MAP;
-    private static final HashMap<String,String> STATUS_PROJECTION_MAP;
+    /** Enum value for a server-initiated sync. */
+    public static final int SOURCE_SERVER = 0;
 
-    private final Context mContext;
-    private final SQLiteOpenHelper mOpenHelper;
-    private static SyncStorageEngine sSyncStorageEngine = null;
+    /** Enum value for a local-initiated sync. */
+    public static final int SOURCE_LOCAL = 1;
+    /**
+     * Enum value for a poll-based sync (e.g., upon connection to
+     * network)
+     */
+    public static final int SOURCE_POLL = 2;
 
-    static {
-        sURLMatcher.addURI("sync", "stats", STATS);
-        sURLMatcher.addURI("sync", "stats/#", STATS_ID);
-        sURLMatcher.addURI("sync", "history", HISTORY);
-        sURLMatcher.addURI("sync", "history/#", HISTORY_ID);
-        sURLMatcher.addURI("sync", "settings", SETTINGS);
-        sURLMatcher.addURI("sync", "status", STATUS);
-        sURLMatcher.addURI("sync", "active", ACTIVE);
-        sURLMatcher.addURI("sync", "pending", PENDING);
+    /** Enum value for a user-initiated sync. */
+    public static final int SOURCE_USER = 3;
 
-        HashMap<String,String> map;
-        PENDING_PROJECTION_MAP = map = new HashMap<String,String>();
-        map.put(Sync.History._ID, Sync.History._ID);
-        map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT);
-        map.put(Sync.History.ACCOUNT_TYPE, Sync.History.ACCOUNT_TYPE);
-        map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY);
+    // TODO: i18n -- grab these out of resources.
+    /** String names for the sync source types. */
+    public static final String[] SOURCES = { "SERVER",
+                                             "LOCAL",
+                                             "POLL",
+                                             "USER" };
 
-        ACTIVE_PROJECTION_MAP = map = new HashMap<String,String>();
-        map.put(Sync.History._ID, Sync.History._ID);
-        map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT);
-        map.put(Sync.History.ACCOUNT_TYPE, Sync.History.ACCOUNT_TYPE);
-        map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY);
-        map.put("startTime", "startTime");
+    // Error types
+    public static final int ERROR_SYNC_ALREADY_IN_PROGRESS = 1;
+    public static final int ERROR_AUTHENTICATION = 2;
+    public static final int ERROR_IO = 3;
+    public static final int ERROR_PARSE = 4;
+    public static final int ERROR_CONFLICT = 5;
+    public static final int ERROR_TOO_MANY_DELETIONS = 6;
+    public static final int ERROR_TOO_MANY_RETRIES = 7;
+    public static final int ERROR_INTERNAL = 8;
 
-        HISTORY_PROJECTION_MAP = map = new HashMap<String,String>();
-        map.put(Sync.History._ID, "history._id as _id");
-        map.put(Sync.History.ACCOUNT, "stats.account as account");
-        map.put(Sync.History.ACCOUNT_TYPE, "stats.account_type as account_type");
-        map.put(Sync.History.AUTHORITY, "stats.authority as authority");
-        map.put(Sync.History.EVENT, Sync.History.EVENT);
-        map.put(Sync.History.EVENT_TIME, Sync.History.EVENT_TIME);
-        map.put(Sync.History.ELAPSED_TIME, Sync.History.ELAPSED_TIME);
-        map.put(Sync.History.SOURCE, Sync.History.SOURCE);
-        map.put(Sync.History.UPSTREAM_ACTIVITY, Sync.History.UPSTREAM_ACTIVITY);
-        map.put(Sync.History.DOWNSTREAM_ACTIVITY, Sync.History.DOWNSTREAM_ACTIVITY);
-        map.put(Sync.History.MESG, Sync.History.MESG);
+    // The MESG column will contain one of these or one of the Error types.
+    public static final String MESG_SUCCESS = "success";
+    public static final String MESG_CANCELED = "canceled";
 
-        STATUS_PROJECTION_MAP = map = new HashMap<String,String>();
-        map.put(Sync.Status._ID, "status._id as _id");
-        map.put(Sync.Status.ACCOUNT, "stats.account as account");
-        map.put(Sync.Status.ACCOUNT_TYPE, "stats.account_type as account_type");
-        map.put(Sync.Status.AUTHORITY, "stats.authority as authority");
-        map.put(Sync.Status.TOTAL_ELAPSED_TIME, Sync.Status.TOTAL_ELAPSED_TIME);
-        map.put(Sync.Status.NUM_SYNCS, Sync.Status.NUM_SYNCS);
-        map.put(Sync.Status.NUM_SOURCE_LOCAL, Sync.Status.NUM_SOURCE_LOCAL);
-        map.put(Sync.Status.NUM_SOURCE_POLL, Sync.Status.NUM_SOURCE_POLL);
-        map.put(Sync.Status.NUM_SOURCE_SERVER, Sync.Status.NUM_SOURCE_SERVER);
-        map.put(Sync.Status.NUM_SOURCE_USER, Sync.Status.NUM_SOURCE_USER);
-        map.put(Sync.Status.LAST_SUCCESS_SOURCE, Sync.Status.LAST_SUCCESS_SOURCE);
-        map.put(Sync.Status.LAST_SUCCESS_TIME, Sync.Status.LAST_SUCCESS_TIME);
-        map.put(Sync.Status.LAST_FAILURE_SOURCE, Sync.Status.LAST_FAILURE_SOURCE);
-        map.put(Sync.Status.LAST_FAILURE_TIME, Sync.Status.LAST_FAILURE_TIME);
-        map.put(Sync.Status.LAST_FAILURE_MESG, Sync.Status.LAST_FAILURE_MESG);
-        map.put(Sync.Status.PENDING, Sync.Status.PENDING);
+    public static final int CHANGE_SETTINGS = 1<<0;
+    public static final int CHANGE_PENDING = 1<<1;
+    public static final int CHANGE_ACTIVE = 1<<2;
+    public static final int CHANGE_STATUS = 1<<3;
+    public static final int CHANGE_ALL = 0x7fffffff;
+    
+    public static final int MAX_HISTORY = 15;
+    
+    private static final int MSG_WRITE_STATUS = 1;
+    private static final long WRITE_STATUS_DELAY = 1000*60*10; // 10 minutes
+    
+    private static final int MSG_WRITE_STATISTICS = 2;
+    private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour
+    
+    public static class PendingOperation {
+        final Account account;
+        final int syncSource;
+        final String authority;
+        final Bundle extras;        // note: read-only.
+        
+        int authorityId;
+        byte[] flatExtras;
+        
+        PendingOperation(Account account, int source,
+                String authority, Bundle extras) {
+            this.account = account;
+            this.syncSource = source;
+            this.authority = authority;
+            this.extras = extras != null ? new Bundle(extras) : extras;
+            this.authorityId = -1;
+        }
+
+        PendingOperation(PendingOperation other) {
+            this.account = other.account;
+            this.syncSource = other.syncSource;
+            this.authority = other.authority;
+            this.extras = other.extras;
+            this.authorityId = other.authorityId;
+        }
     }
+    
+    static class AccountInfo {
+        final Account account;
+        final HashMap<String, AuthorityInfo> authorities =
+                new HashMap<String, AuthorityInfo>();
+        
+        AccountInfo(Account account) {
+            this.account = account;
+        }
+    }
+    
+    public static class AuthorityInfo {
+        final Account account;
+        final String authority;
+        final int ident;
+        boolean enabled;
+        
+        AuthorityInfo(Account account, String authority, int ident) {
+            this.account = account;
+            this.authority = authority;
+            this.ident = ident;
+            enabled = true;
+        }
+    }
+    
+    public static class SyncHistoryItem {
+        int authorityId;
+        int historyId;
+        long eventTime;
+        long elapsedTime;
+        int source;
+        int event;
+        long upstreamActivity;
+        long downstreamActivity;
+        String mesg;
+    }
+    
+    public static class DayStats {
+        public final int day;
+        public int successCount;
+        public long successTime;
+        public int failureCount;
+        public long failureTime;
+        
+        public DayStats(int day) {
+            this.day = day;
+        }
+    }
+    
+    // Primary list of all syncable authorities.  Also our global lock.
+    private final SparseArray<AuthorityInfo> mAuthorities =
+            new SparseArray<AuthorityInfo>();
+    
+    private final HashMap<Account, AccountInfo> mAccounts =
+        new HashMap<Account, AccountInfo>();
 
-    private static final String[] STATS_ACCOUNT_PROJECTION =
-            new String[] { Sync.Stats.ACCOUNT, Sync.Stats.ACCOUNT_TYPE };
-
-    private static final int MAX_HISTORY_EVENTS_TO_KEEP = 5000;
-
-    private static final String SELECT_INITIAL_FAILURE_TIME_QUERY_STRING = ""
-            + "SELECT min(a) "
-            + "FROM ("
-            + "  SELECT initialFailureTime AS a "
-            + "  FROM status "
-            + "  WHERE stats_id=? AND a IS NOT NULL "
-            + "    UNION "
-            + "  SELECT ? AS a"
-            + " )";
-
+    private final ArrayList<PendingOperation> mPendingOperations =
+            new ArrayList<PendingOperation>();
+    
+    private ActiveSyncInfo mActiveSync;
+    
+    private final SparseArray<SyncStatusInfo> mSyncStatus =
+            new SparseArray<SyncStatusInfo>();
+    
+    private final ArrayList<SyncHistoryItem> mSyncHistory =
+            new ArrayList<SyncHistoryItem>();
+    
+    private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners
+            = new RemoteCallbackList<ISyncStatusObserver>();
+    
+    // We keep 4 weeks of stats.
+    private final DayStats[] mDayStats = new DayStats[7*4];
+    private final Calendar mCal;
+    private int mYear;
+    private int mYearInDays;
+    
+    private final Context mContext;
+    private static volatile SyncStorageEngine sSyncStorageEngine = null;
+    
+    /**
+     * This file contains the core engine state: all accounts and the
+     * settings for them.  It must never be lost, and should be changed
+     * infrequently, so it is stored as an XML file.
+     */
+    private final AtomicFile mAccountInfoFile;
+    
+    /**
+     * This file contains the current sync status.  We would like to retain
+     * it across boots, but its loss is not the end of the world, so we store
+     * this information as binary data.
+     */
+    private final AtomicFile mStatusFile;
+    
+    /**
+     * This file contains sync statistics.  This is purely debugging information
+     * so is written infrequently and can be thrown away at any time.
+     */
+    private final AtomicFile mStatisticsFile;
+    
+    /**
+     * This file contains the pending sync operations.  It is a binary file,
+     * which must be updated every time an operation is added or removed,
+     * so we have special handling of it.
+     */
+    private final AtomicFile mPendingFile;
+    private static final int PENDING_FINISH_TO_WRITE = 4;
+    private int mNumPendingFinished = 0;
+    
+    private int mNextHistoryId = 0;
+    private boolean mListenForTickles = true;
+    
     private SyncStorageEngine(Context context) {
         mContext = context;
-        mOpenHelper = new SyncStorageEngine.DatabaseHelper(context);
         sSyncStorageEngine = this;
+        
+        mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
+        
+        File dataDir = Environment.getDataDirectory();
+        File systemDir = new File(dataDir, "system");
+        File syncDir = new File(systemDir, "sync");
+        mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"));
+        mStatusFile = new AtomicFile(new File(syncDir, "status.bin"));
+        mPendingFile = new AtomicFile(new File(syncDir, "pending.bin"));
+        mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin"));
+        
+        readAccountInfoLocked();
+        readStatusLocked();
+        readPendingOperationsLocked();
+        readStatisticsLocked();
+        readLegacyAccountInfoLocked();
     }
 
     public static SyncStorageEngine newTestInstance(Context context) {
@@ -147,650 +300,1263 @@
         return sSyncStorageEngine;
     }
 
-    private class DatabaseHelper extends SQLiteOpenHelper {
-        DatabaseHelper(Context context) {
-            super(context, DATABASE_NAME, null, DATABASE_VERSION);
-        }
-
-        @Override
-        public void onCreate(SQLiteDatabase db) {
-            db.execSQL("CREATE TABLE pending ("
-                    + "_id INTEGER PRIMARY KEY,"
-                    + "authority TEXT NOT NULL,"
-                    + "account TEXT NOT NULL,"
-                    + "account_type TEXT NOT NULL,"
-                    + "extras BLOB NOT NULL,"
-                    + "source INTEGER NOT NULL"
-                    + ");");
-
-            db.execSQL("CREATE TABLE stats (" +
-                       "_id INTEGER PRIMARY KEY," +
-                       "account TEXT, " +
-                       "account_type TEXT, " +
-                       "authority TEXT, " +
-                       "syncdata TEXT, " +
-                       "UNIQUE (account, authority)" +
-                       ");");
-
-            db.execSQL("CREATE TABLE history (" +
-                       "_id INTEGER PRIMARY KEY," +
-                       "stats_id INTEGER," +
-                       "eventTime INTEGER," +
-                       "elapsedTime INTEGER," +
-                       "source INTEGER," +
-                       "event INTEGER," +
-                       "upstreamActivity INTEGER," +
-                       "downstreamActivity INTEGER," +
-                       "mesg TEXT);");
-
-            db.execSQL("CREATE TABLE status ("
-                    + "_id INTEGER PRIMARY KEY,"
-                    + "stats_id INTEGER NOT NULL,"
-                    + "totalElapsedTime INTEGER NOT NULL DEFAULT 0,"
-                    + "numSyncs INTEGER NOT NULL DEFAULT 0,"
-                    + "numSourcePoll INTEGER NOT NULL DEFAULT 0,"
-                    + "numSourceServer INTEGER NOT NULL DEFAULT 0,"
-                    + "numSourceLocal INTEGER NOT NULL DEFAULT 0,"
-                    + "numSourceUser INTEGER NOT NULL DEFAULT 0,"
-                    + "lastSuccessTime INTEGER,"
-                    + "lastSuccessSource INTEGER,"
-                    + "lastFailureTime INTEGER,"
-                    + "lastFailureSource INTEGER,"
-                    + "lastFailureMesg STRING,"
-                    + "initialFailureTime INTEGER,"
-                    + "pending INTEGER NOT NULL DEFAULT 0);");
-
-            db.execSQL("CREATE TABLE active ("
-                    + "_id INTEGER PRIMARY KEY,"
-                    + "authority TEXT,"
-                    + "account TEXT,"
-                    + "account_type TEXT,"
-                    + "startTime INTEGER);");
-
-            db.execSQL("CREATE INDEX historyEventTime ON history (eventTime)");
-
-            db.execSQL("CREATE TABLE settings (" +
-                       "name TEXT PRIMARY KEY," +
-                       "value TEXT);");
-        }
-
-        @Override
-        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            if (oldVersion == 9) {
-                Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
-                        + newVersion + ", which will preserve old data");
-                db.execSQL("ALTER TABLE status ADD COLUMN initialFailureTime INTEGER");
-                oldVersion++;
+    @Override public void handleMessage(Message msg) {
+        if (msg.what == MSG_WRITE_STATUS) {
+            synchronized (mAccounts) {
+                writeStatusLocked();
             }
-
-            if (oldVersion == 10) {
-                Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
-                        + newVersion + ", which will preserve old data");
-                db.execSQL("ALTER TABLE pending ADD COLUMN account_type TEXT");
-                db.execSQL("ALTER TABLE stats ADD COLUMN account_type TEXT");
-                db.execSQL("ALTER TABLE active ADD COLUMN account_type TEXT");
-
-                db.execSQL("UPDATE pending SET account_type='com.google.GAIA'");
-                db.execSQL("UPDATE stats SET account_type='com.google.GAIA'");
-                db.execSQL("UPDATE active SET account_type='com.google.GAIA'");
-                oldVersion++;
-            }
-
-            if (oldVersion == newVersion) {
-                return;
-            }
-
-            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
-                    + newVersion + ", which will destroy all old data");
-            db.execSQL("DROP TABLE IF EXISTS pending");
-            db.execSQL("DROP TABLE IF EXISTS stats");
-            db.execSQL("DROP TABLE IF EXISTS history");
-            db.execSQL("DROP TABLE IF EXISTS settings");
-            db.execSQL("DROP TABLE IF EXISTS active");
-            db.execSQL("DROP TABLE IF EXISTS status");
-            onCreate(db);
-        }
-
-        @Override
-        public void onOpen(SQLiteDatabase db) {
-            if (!db.isReadOnly()) {
-                db.delete("active", null, null);
-                db.insert("active", "account", null);
+        } else if (msg.what == MSG_WRITE_STATISTICS) {
+            synchronized (mAccounts) {
+                writeStatisticsLocked();
             }
         }
     }
+    
+    public void addStatusChangeListener(int mask, ISyncStatusObserver callback) {
+        synchronized (mAuthorities) {
+            mChangeListeners.register(callback, mask);
+        }
+    }
+    
+    public void removeStatusChangeListener(ISyncStatusObserver callback) {
+        synchronized (mAuthorities) {
+            mChangeListeners.unregister(callback);
+        }
+    }
+    
+    private void reportChange(int which) {
+        ArrayList<ISyncStatusObserver> reports = null;
+        synchronized (mAuthorities) {
+            int i = mChangeListeners.beginBroadcast();
+            while (i > 0) {
+                i--;
+                Integer mask = (Integer)mChangeListeners.getBroadcastCookie(i);
+                if ((which & mask.intValue()) == 0) {
+                    continue;
+                }
+                if (reports == null) {
+                    reports = new ArrayList<ISyncStatusObserver>(i);
+                }
+                reports.add(mChangeListeners.getBroadcastItem(i));
+            }
+        }
+        
+        if (DEBUG) Log.v(TAG, "reportChange " + which + " to: " + reports);
+        
+        if (reports != null) {
+            int i = reports.size();
+            while (i > 0) {
+                i--;
+                try {
+                    reports.get(i).onStatusChanged(which);
+                } catch (RemoteException e) {
+                    // The remote callback list will take care of this for us.
+                }
+            }
+        }
+    }
+    
+    public boolean getSyncProviderAutomatically(Account account, String providerName) {
+        synchronized (mAuthorities) {
+            if (account != null) {
+                AuthorityInfo authority = getAuthorityLocked(account, providerName,
+                        "getSyncProviderAutomatically");
+                return authority != null ? authority.enabled : false;
+            }
+            
+            int i = mAuthorities.size();
+            while (i > 0) {
+                i--;
+                AuthorityInfo authority = mAuthorities.get(i);
+                if (authority.authority.equals(providerName)
+                        && authority.enabled) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
 
-    protected void doDatabaseCleanup(Account[] accounts) {
-        HashSet<Account> currentAccounts = Sets.newHashSet(accounts);
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        Cursor cursor = db.query("stats", STATS_ACCOUNT_PROJECTION,
-                null /* where */, null /* where args */,
-                Sync.Stats.ACCOUNT + "," + Sync.Stats.ACCOUNT_TYPE,
-                null /* having */, null /* order by */);
-        try {
-            while (cursor.moveToNext()) {
-                String accountName = cursor.getString(0);
-                String accountType = cursor.getString(1);
-                final Account account = new Account(accountName, accountType);
-                if (!currentAccounts.contains(account)) {
-                    String where = Sync.Stats.ACCOUNT + "=? AND " + Sync.Stats.ACCOUNT_TYPE + "=?";
-                    int numDeleted;
-                    numDeleted = db.delete("stats", where,
-                            new String[]{account.mName, account.mType});
-                    if (Config.LOGD) {
-                        Log.d(TAG, "deleted " + numDeleted
-                                + " records from stats table"
-                                + " for account " + account);
+    public void setSyncProviderAutomatically(Account account, String providerName,
+            boolean sync) {
+        synchronized (mAuthorities) {
+            if (account != null) {
+                AuthorityInfo authority = getAuthorityLocked(account, providerName,
+                        "setSyncProviderAutomatically");
+                if (authority != null) {
+                    authority.enabled = sync;
+                }
+            } else {
+                int i = mAuthorities.size();
+                while (i > 0) {
+                    i--;
+                    AuthorityInfo authority = mAuthorities.get(i);
+                    if (authority.account.equals(account)
+                            && authority.authority.equals(providerName)) {
+                        authority.enabled = sync;
                     }
                 }
             }
-        } finally {
-            cursor.close();
+            writeAccountInfoLocked();
         }
-    }
-
-    protected void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
-        if (activeSyncContext != null) {
-            updateActiveSync(activeSyncContext.mSyncOperation.account,
-                    activeSyncContext.mSyncOperation.authority, activeSyncContext.mStartTime);
-        } else {
-            // we indicate that the sync is not active by passing null for all the parameters
-            updateActiveSync(null, null, null);
-        }
-    }
-
-    private int updateActiveSync(Account account, String authority, Long startTime) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        ContentValues values = new ContentValues();
-        values.put("account", account == null ? null : account.mName);
-        values.put("account_type", account == null ? null : account.mType);
-        values.put("authority", authority);
-        values.put("startTime", startTime);
-        int numChanges = db.update("active", values, null, null);
-        if (numChanges > 0) {
-            mContext.getContentResolver().notifyChange(Sync.Active.CONTENT_URI,
-                    null /* this change wasn't made through an observer */);
-        }
-        return numChanges;
-    }
-
-    /**
-     * Implements the {@link ContentProvider#query} method
-     */
-    public Cursor query(Uri url, String[] projectionIn,
-            String selection, String[] selectionArgs, String sort) {
-        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-
-        // Generate the body of the query
-        int match = sURLMatcher.match(url);
-        String groupBy = null;
-        switch (match) {
-            case STATS:
-                qb.setTables("stats");
-                break;
-            case STATS_ID:
-                qb.setTables("stats");
-                qb.appendWhere("_id=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                break;
-            case HISTORY:
-                // join the stats and history tables, so the caller can get
-                // the account and authority information as part of this query.
-                qb.setTables("stats, history");
-                qb.setProjectionMap(HISTORY_PROJECTION_MAP);
-                qb.appendWhere("stats._id = history.stats_id");
-                break;
-            case ACTIVE:
-                qb.setTables("active");
-                qb.setProjectionMap(ACTIVE_PROJECTION_MAP);
-                qb.appendWhere("account is not null");
-                break;
-            case PENDING:
-                qb.setTables("pending");
-                qb.setProjectionMap(PENDING_PROJECTION_MAP);
-                groupBy = "account, authority";
-                break;
-            case STATUS:
-                // join the stats and status tables, so the caller can get
-                // the account and authority information as part of this query.
-                qb.setTables("stats, status");
-                qb.setProjectionMap(STATUS_PROJECTION_MAP);
-                qb.appendWhere("stats._id = status.stats_id");
-                break;
-            case HISTORY_ID:
-                // join the stats and history tables, so the caller can get
-                // the account and authority information as part of this query.
-                qb.setTables("stats, history");
-                qb.setProjectionMap(HISTORY_PROJECTION_MAP);
-                qb.appendWhere("stats._id = history.stats_id");
-                qb.appendWhere("AND history._id=");
-                qb.appendWhere(url.getPathSegments().get(1));
-                break;
-            case SETTINGS:
-                qb.setTables("settings");
-                break;
-            default:
-                throw new IllegalArgumentException("Unknown URL " + url);
-        }
-
-        if (match == SETTINGS) {
-            mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
-                    "no permission to read the sync settings");
-        } else {
-            mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
-                    "no permission to read the sync stats");
-        }
-        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
-        Cursor c = qb.query(db, projectionIn, selection, selectionArgs, groupBy, null, sort);
-        c.setNotificationUri(mContext.getContentResolver(), url);
-        return c;
-    }
-
-    /**
-     * Implements the {@link ContentProvider#insert} method
-     * @param callerIsTheProvider true if this is being called via the
-     *  {@link ContentProvider#insert} in method rather than directly.
-     * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
-     *   for the Settings table.
-     */
-    public Uri insert(boolean callerIsTheProvider, Uri url, ContentValues values) {
-        String table;
-        long rowID;
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        final int match = sURLMatcher.match(url);
-        checkCaller(callerIsTheProvider, match);
-        switch (match) {
-            case SETTINGS:
-                mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
-                        "no permission to write the sync settings");
-                table = "settings";
-                rowID = db.replace(table, null, values);
-                break;
-            default:
-                throw new IllegalArgumentException("Unknown URL " + url);
-        }
-
-
-        if (rowID > 0) {
-            mContext.getContentResolver().notifyChange(url, null /* observer */);
-            return Uri.parse("content://sync/" + table + "/" + rowID);
-        }
-
-        return null;
-    }
-
-    private static void checkCaller(boolean callerIsTheProvider, int match) {
-        if (callerIsTheProvider && match != SETTINGS) {
-            throw new UnsupportedOperationException(
-                    "only the settings are modifiable via the ContentProvider interface");
-        }
-    }
-
-    /**
-     * Implements the {@link ContentProvider#delete} method
-     * @param callerIsTheProvider true if this is being called via the
-     *  {@link ContentProvider#delete} in method rather than directly.
-     * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
-     *   for the Settings table.
-     */
-    public int delete(boolean callerIsTheProvider, Uri url, String where, String[] whereArgs) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        int match = sURLMatcher.match(url);
-
-        int numRows;
-        switch (match) {
-            case SETTINGS:
-                mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
-                        "no permission to write the sync settings");
-                numRows = db.delete("settings", where, whereArgs);
-                break;
-            default:
-                throw new UnsupportedOperationException("Cannot delete URL: " + url);
-        }
-
-        if (numRows > 0) {
-            mContext.getContentResolver().notifyChange(url, null /* observer */);
-        }
-        return numRows;
-    }
-
-    /**
-     * Implements the {@link ContentProvider#update} method
-     * @param callerIsTheProvider true if this is being called via the
-     *  {@link ContentProvider#update} in method rather than directly.
-     * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
-     *   for the Settings table.
-     */
-    public int update(boolean callerIsTheProvider, Uri url, ContentValues initialValues,
-            String where, String[] whereArgs) {
-        switch (sURLMatcher.match(url)) {
-            case SETTINGS:
-                throw new UnsupportedOperationException("updating url " + url
-                        + " is not allowed, use insert instead");
-            default:
-                throw new UnsupportedOperationException("Cannot update URL: " + url);
-        }
-    }
-
-    /**
-     * Implements the {@link ContentProvider#getType} method
-     */
-    public String getType(Uri url) {
-        int match = sURLMatcher.match(url);
-        switch (match) {
-            case SETTINGS:
-                return "vnd.android.cursor.dir/sync-settings";
-            default:
-                throw new IllegalArgumentException("Unknown URL");
-        }
-    }
-
-    protected Uri insertIntoPending(ContentValues values) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        try {
-            db.beginTransaction();
-            long rowId = db.insert("pending", Sync.Pending.ACCOUNT, values);
-            if (rowId < 0) return null;
-            String accountName = values.getAsString(Sync.Pending.ACCOUNT);
-            String accountType = values.getAsString(Sync.Pending.ACCOUNT_TYPE);
-            final Account account = new Account(accountName, accountType);
-            String authority = values.getAsString(Sync.Pending.AUTHORITY);
-
-            long statsId = createStatsRowIfNecessary(account, authority);
-            createStatusRowIfNecessary(statsId);
-
-            values.clear();
-            values.put(Sync.Status.PENDING, 1);
-            int numUpdatesStatus = db.update("status", values, "stats_id=" + statsId, null);
-
-            db.setTransactionSuccessful();
-
-            mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
-                    null /* no observer initiated this change */);
-            if (numUpdatesStatus > 0) {
-                mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
-                        null /* no observer initiated this change */);
-            }
-            return ContentUris.withAppendedId(Sync.Pending.CONTENT_URI, rowId);
-        } finally {
-            db.endTransaction();
-        }
-    }
-
-    int deleteFromPending(long rowId) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        db.beginTransaction();
-        try {
-            Account account;
-            String authority;
-            Cursor c = db.query("pending",
-                    new String[]{Sync.Pending.ACCOUNT, Sync.Pending.ACCOUNT_TYPE,
-                            Sync.Pending.AUTHORITY},
-                    "_id=" + rowId, null, null, null, null);
-            try {
-                if (c.getCount() != 1) {
-                    return 0;
-                }
-                c.moveToNext();
-                String accountName = c.getString(0);
-                String accountType = c.getString(1);
-                account = new Account(accountName, accountType);
-                authority = c.getString(2);
-            } finally {
-                c.close();
-            }
-            db.delete("pending", "_id=" + rowId, null /* no where args */);
-            final String[] accountAuthorityWhereArgs =
-                    new String[]{account.mName, account.mType, authority};
-            boolean isPending = 0 < DatabaseUtils.longForQuery(db,
-                    "SELECT COUNT(*)"
-                            + " FROM PENDING"
-                            + " WHERE account=? AND account_type=? AND authority=?",
-                    accountAuthorityWhereArgs);
-            if (!isPending) {
-                long statsId = createStatsRowIfNecessary(account, authority);
-                db.execSQL("UPDATE status SET pending=0 WHERE stats_id=" + statsId);
-            }
-            db.setTransactionSuccessful();
-
-            mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
-                    null /* no observer initiated this change */);
-            if (!isPending) {
-                mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
-                        null /* no observer initiated this change */);
-            }
-            return 1;
-        } finally {
-            db.endTransaction();
-        }
-    }
-
-    int clearPending() {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        db.beginTransaction();
-        try {
-            int numChanges = db.delete("pending", null, null /* no where args */);
-            if (numChanges > 0) {
-                db.execSQL("UPDATE status SET pending=0");
-                mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
-                        null /* no observer initiated this change */);
-                mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
-                        null /* no observer initiated this change */);
-            }
-            db.setTransactionSuccessful();
-            return numChanges;
-        } finally {
-            db.endTransaction();
-        }
-    }
-
-    /**
-     * Returns a cursor over all the pending syncs in no particular order. This cursor is not
-     * "live", in that if changes are made to the pending table any observers on this cursor
-     * will not be notified.
-     * @param projection Return only these columns. If null then all columns are returned.
-     * @return the cursor of pending syncs
-     */
-    public Cursor getPendingSyncsCursor(String[] projection) {
-        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
-        return db.query("pending", projection, null, null, null, null, null);
-    }
-
-    // @VisibleForTesting
-    static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
-
-    private boolean purgeOldHistoryEvents(long now) {
-        // remove events that are older than MILLIS_IN_4WEEKS
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        int numDeletes = db.delete("history", "eventTime<" + (now - MILLIS_IN_4WEEKS), null);
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            if (numDeletes > 0) {
-                Log.v(TAG, "deleted " + numDeletes + " old event(s) from the sync history");
-            }
-        }
-
-        // keep only the last MAX_HISTORY_EVENTS_TO_KEEP history events
-        numDeletes += db.delete("history", "eventTime < (select min(eventTime) from "
-                + "(select eventTime from history order by eventTime desc limit ?))",
-                new String[]{String.valueOf(MAX_HISTORY_EVENTS_TO_KEEP)});
         
-        return numDeletes > 0;
+        reportChange(CHANGE_SETTINGS);
     }
 
-    public long insertStartSyncEvent(Account account, String authority, long now, int source) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        long statsId = createStatsRowIfNecessary(account, authority);
+    public void setListenForNetworkTickles(boolean flag) {
+        synchronized (mAuthorities) {
+            mListenForTickles = flag;
+            writeAccountInfoLocked();
+        }
+        reportChange(CHANGE_SETTINGS);
+    }
 
-        purgeOldHistoryEvents(now);
-        ContentValues values = new ContentValues();
-        values.put(Sync.History.STATS_ID, statsId);
-        values.put(Sync.History.EVENT_TIME, now);
-        values.put(Sync.History.SOURCE, source);
-        values.put(Sync.History.EVENT, Sync.History.EVENT_START);
-        long rowId = db.insert("history", null, values);
-        mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI, null /* observer */);
-        mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, null /* observer */);
-        return rowId;
+    public boolean getListenForNetworkTickles() {
+        synchronized (mAuthorities) {
+            return mListenForTickles;
+        }
+    }
+    
+    public AuthorityInfo getAuthority(Account account, String authority) {
+        synchronized (mAuthorities) {
+            return getAuthorityLocked(account, authority, null);
+        }
+    }
+    
+    public AuthorityInfo getAuthority(int authorityId) {
+        synchronized (mAuthorities) {
+            return mAuthorities.get(authorityId);
+        }
+    }
+    
+    /**
+     * Returns true if there is currently a sync operation for the given
+     * account or authority in the pending list, or actively being processed.
+     */
+    public boolean isSyncActive(Account account, String authority) {
+        synchronized (mAuthorities) {
+            int i = mPendingOperations.size();
+            while (i > 0) {
+                i--;
+                // TODO(fredq): this probably shouldn't be considering
+                // pending operations.
+                PendingOperation op = mPendingOperations.get(i);
+                if (op.account.equals(account) && op.authority.equals(authority)) {
+                    return true;
+                }
+            }
+            
+            if (mActiveSync != null) {
+                AuthorityInfo ainfo = getAuthority(mActiveSync.authorityId);
+                if (ainfo != null && ainfo.account.equals(account)
+                        && ainfo.authority.equals(authority)) {
+                    return true;
+                }
+            }
+        }
+        
+        return false;
+    }
+    
+    public PendingOperation insertIntoPending(PendingOperation op) {
+        synchronized (mAuthorities) {
+            if (DEBUG) Log.v(TAG, "insertIntoPending: account=" + op.account
+                    + " auth=" + op.authority
+                    + " src=" + op.syncSource
+                    + " extras=" + op.extras);
+            
+            AuthorityInfo authority = getOrCreateAuthorityLocked(op.account,
+                    op.authority,
+                    -1 /* desired identifier */,
+                    true /* write accounts to storage */);
+            if (authority == null) {
+                return null;
+            }
+            
+            op = new PendingOperation(op);
+            op.authorityId = authority.ident;
+            mPendingOperations.add(op);
+            appendPendingOperationLocked(op);
+            
+            SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
+            status.pending = true;
+        }
+        
+        reportChange(CHANGE_PENDING);
+        return op;
+    }
+
+    public boolean deleteFromPending(PendingOperation op) {
+        boolean res = false;
+        synchronized (mAuthorities) {
+            if (DEBUG) Log.v(TAG, "deleteFromPending: account=" + op.account
+                    + " auth=" + op.authority
+                    + " src=" + op.syncSource
+                    + " extras=" + op.extras);
+            if (mPendingOperations.remove(op)) {
+                if (mPendingOperations.size() == 0
+                        || mNumPendingFinished >= PENDING_FINISH_TO_WRITE) {
+                    writePendingOperationsLocked();
+                    mNumPendingFinished = 0;
+                } else {
+                    mNumPendingFinished++;
+                }
+                
+                AuthorityInfo authority = getAuthorityLocked(op.account, op.authority,
+                        "deleteFromPending");
+                if (authority != null) {
+                    if (DEBUG) Log.v(TAG, "removing - " + authority);
+                    final int N = mPendingOperations.size();
+                    boolean morePending = false;
+                    for (int i=0; i<N; i++) {
+                        PendingOperation cur = mPendingOperations.get(i);
+                        if (cur.account.equals(op.account)
+                                && cur.authority.equals(op.authority)) {
+                            morePending = true;
+                            break;
+                        }
+                    }
+                    
+                    if (!morePending) {
+                        if (DEBUG) Log.v(TAG, "no more pending!");
+                        SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
+                        status.pending = false;
+                    }
+                }
+                
+                res = true;
+            }
+        }
+        
+        reportChange(CHANGE_PENDING);
+        return res;
+    }
+
+    public int clearPending() {
+        int num;
+        synchronized (mAuthorities) {
+            if (DEBUG) Log.v(TAG, "clearPending");
+            num = mPendingOperations.size();
+            mPendingOperations.clear();
+            final int N = mSyncStatus.size();
+            for (int i=0; i<N; i++) {
+                mSyncStatus.get(i).pending = false;
+            }
+            writePendingOperationsLocked();
+        }
+        reportChange(CHANGE_PENDING);
+        return num;
+    }
+
+    /**
+     * Return a copy of the current array of pending operations.  The
+     * PendingOperation objects are the real objects stored inside, so that
+     * they can be used with deleteFromPending().
+     */
+    public ArrayList<PendingOperation> getPendingOperations() {
+        synchronized (mAuthorities) {
+            return new ArrayList<PendingOperation>(mPendingOperations);
+        }
+    }
+    
+    /**
+     * Return the number of currently pending operations.
+     */
+    public int getPendingOperationCount() {
+        synchronized (mAuthorities) {
+            return mPendingOperations.size();
+        }
+    }
+    
+    /**
+     * Called when the set of account has changed, given the new array of
+     * active accounts.
+     */
+    public void doDatabaseCleanup(Account[] accounts) {
+        synchronized (mAuthorities) {
+            if (DEBUG) Log.w(TAG, "Updating for new accounts...");
+            SparseArray<AuthorityInfo> removing = new SparseArray<AuthorityInfo>();
+            Iterator<AccountInfo> accIt = mAccounts.values().iterator();
+            while (accIt.hasNext()) {
+                AccountInfo acc = accIt.next();
+                if (!ArrayUtils.contains(accounts, acc.account)) {
+                    // This account no longer exists...
+                    if (DEBUG) Log.w(TAG, "Account removed: " + acc.account);
+                    for (AuthorityInfo auth : acc.authorities.values()) {
+                        removing.put(auth.ident, auth);
+                    }
+                    accIt.remove();
+                }
+            }
+            
+            // Clean out all data structures.
+            int i = removing.size();
+            if (i > 0) {
+                while (i > 0) {
+                    i--;
+                    int ident = removing.keyAt(i);
+                    mAuthorities.remove(ident);
+                    int j = mSyncStatus.size();
+                    while (j > 0) {
+                        j--;
+                        if (mSyncStatus.keyAt(j) == ident) {
+                            mSyncStatus.remove(mSyncStatus.keyAt(j));
+                        }
+                    }
+                    j = mSyncHistory.size();
+                    while (j > 0) {
+                        j--;
+                        if (mSyncHistory.get(j).authorityId == ident) {
+                            mSyncHistory.remove(j);
+                        }
+                    }
+                }
+                writeAccountInfoLocked();
+                writeStatusLocked();
+                writePendingOperationsLocked();
+                writeStatisticsLocked();
+            }
+        }
+    }
+
+    /**
+     * Called when the currently active sync is changing (there can only be
+     * one at a time).  Either supply a valid ActiveSyncContext with information
+     * about the sync, or null to stop the currently active sync.
+     */
+    public void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
+        synchronized (mAuthorities) {
+            if (activeSyncContext != null) {
+                if (DEBUG) Log.v(TAG, "setActiveSync: account="
+                        + activeSyncContext.mSyncOperation.account
+                        + " auth=" + activeSyncContext.mSyncOperation.authority
+                        + " src=" + activeSyncContext.mSyncOperation.syncSource
+                        + " extras=" + activeSyncContext.mSyncOperation.extras);
+                if (mActiveSync != null) {
+                    Log.w(TAG, "setActiveSync called with existing active sync!");
+                }
+                AuthorityInfo authority = getAuthorityLocked(
+                        activeSyncContext.mSyncOperation.account,
+                        activeSyncContext.mSyncOperation.authority,
+                        "setActiveSync");
+                if (authority == null) {
+                    return;
+                }
+                mActiveSync = new ActiveSyncInfo(authority.ident,
+                        authority.account, authority.authority,
+                        activeSyncContext.mStartTime);
+            } else {
+                if (DEBUG) Log.v(TAG, "setActiveSync: null");
+                mActiveSync = null;
+            }
+        }
+        
+        reportChange(CHANGE_ACTIVE);
+    }
+
+    /**
+     * To allow others to send active change reports, to poke clients.
+     */
+    public void reportActiveChange() {
+        reportChange(CHANGE_ACTIVE);
+    }
+    
+    /**
+     * Note that sync has started for the given account and authority.
+     */
+    public long insertStartSyncEvent(Account accountName, String authorityName,
+            long now, int source) {
+        long id;
+        synchronized (mAuthorities) {
+            if (DEBUG) Log.v(TAG, "insertStartSyncEvent: account=" + accountName
+                    + " auth=" + authorityName + " source=" + source);
+            AuthorityInfo authority = getAuthorityLocked(accountName, authorityName,
+                    "insertStartSyncEvent");
+            if (authority == null) {
+                return -1;
+            }
+            SyncHistoryItem item = new SyncHistoryItem();
+            item.authorityId = authority.ident;
+            item.historyId = mNextHistoryId++;
+            if (mNextHistoryId < 0) mNextHistoryId = 0;
+            item.eventTime = now;
+            item.source = source;
+            item.event = EVENT_START;
+            mSyncHistory.add(0, item);
+            while (mSyncHistory.size() > MAX_HISTORY) {
+                mSyncHistory.remove(mSyncHistory.size()-1);
+            }
+            id = item.historyId;
+            if (DEBUG) Log.v(TAG, "returning historyId " + id);
+        }
+        
+        reportChange(CHANGE_STATUS);
+        return id;
     }
 
     public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage,
             long downstreamActivity, long upstreamActivity) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        db.beginTransaction();
-        try {
-            ContentValues values = new ContentValues();
-            values.put(Sync.History.ELAPSED_TIME, elapsedTime);
-            values.put(Sync.History.EVENT, Sync.History.EVENT_STOP);
-            values.put(Sync.History.MESG, resultMessage);
-            values.put(Sync.History.DOWNSTREAM_ACTIVITY, downstreamActivity);
-            values.put(Sync.History.UPSTREAM_ACTIVITY, upstreamActivity);
-
-            int count = db.update("history", values, "_id=?",
-                    new String[]{Long.toString(historyId)});
-            // We think that count should always be 1 but don't want to change this until after
-            // launch.
-            if (count > 0) {
-                int source = (int) DatabaseUtils.longForQuery(db,
-                        "SELECT source FROM history WHERE _id=" + historyId, null);
-                long eventTime = DatabaseUtils.longForQuery(db,
-                        "SELECT eventTime FROM history WHERE _id=" + historyId, null);
-                long statsId = DatabaseUtils.longForQuery(db,
-                        "SELECT stats_id FROM history WHERE _id=" + historyId, null);
-
-                createStatusRowIfNecessary(statsId);
-
-                // update the status table to reflect this sync
-                StringBuilder sb = new StringBuilder();
-                ArrayList<String> bindArgs = new ArrayList<String>();
-                sb.append("UPDATE status SET");
-                sb.append(" numSyncs=numSyncs+1");
-                sb.append(", totalElapsedTime=totalElapsedTime+" + elapsedTime);
-                switch (source) {
-                    case Sync.History.SOURCE_LOCAL:
-                        sb.append(", numSourceLocal=numSourceLocal+1");
-                        break;
-                    case Sync.History.SOURCE_POLL:
-                        sb.append(", numSourcePoll=numSourcePoll+1");
-                        break;
-                    case Sync.History.SOURCE_USER:
-                        sb.append(", numSourceUser=numSourceUser+1");
-                        break;
-                    case Sync.History.SOURCE_SERVER:
-                        sb.append(", numSourceServer=numSourceServer+1");
-                        break;
+        synchronized (mAuthorities) {
+            if (DEBUG) Log.v(TAG, "stopSyncEvent: historyId=" + historyId);
+            SyncHistoryItem item = null;
+            int i = mSyncHistory.size();
+            while (i > 0) {
+                i--;
+                item = mSyncHistory.get(i);
+                if (item.historyId == historyId) {
+                    break;
                 }
-
-                final String statsIdString = String.valueOf(statsId);
-                final long lastSyncTime = (eventTime + elapsedTime);
-                if (Sync.History.MESG_SUCCESS.equals(resultMessage)) {
-                    // - if successful, update the successful columns
-                    sb.append(", lastSuccessTime=" + lastSyncTime);
-                    sb.append(", lastSuccessSource=" + source);
-                    sb.append(", lastFailureTime=null");
-                    sb.append(", lastFailureSource=null");
-                    sb.append(", lastFailureMesg=null");
-                    sb.append(", initialFailureTime=null");
-                } else if (!Sync.History.MESG_CANCELED.equals(resultMessage)) {
-                    sb.append(", lastFailureTime=" + lastSyncTime);
-                    sb.append(", lastFailureSource=" + source);
-                    sb.append(", lastFailureMesg=?");
-                    bindArgs.add(resultMessage);
-                    long initialFailureTime = DatabaseUtils.longForQuery(db,
-                            SELECT_INITIAL_FAILURE_TIME_QUERY_STRING, 
-                            new String[]{statsIdString, String.valueOf(lastSyncTime)});
-                    sb.append(", initialFailureTime=" + initialFailureTime);
-                }
-                sb.append(" WHERE stats_id=?");
-                bindArgs.add(statsIdString);
-                db.execSQL(sb.toString(), bindArgs.toArray());
-                db.setTransactionSuccessful();
-                mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI,
-                        null /* observer */);
-                mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
-                        null /* observer */);
+                item = null;
             }
-        } finally {
-            db.endTransaction();
+            
+            if (item == null) {
+                Log.w(TAG, "stopSyncEvent: no history for id " + historyId);
+                return;
+            }
+            
+            item.elapsedTime = elapsedTime;
+            item.event = EVENT_STOP;
+            item.mesg = resultMessage;
+            item.downstreamActivity = downstreamActivity;
+            item.upstreamActivity = upstreamActivity;
+            
+            SyncStatusInfo status = getOrCreateSyncStatusLocked(item.authorityId);
+            
+            status.numSyncs++;
+            status.totalElapsedTime += elapsedTime;
+            switch (item.source) {
+                case SOURCE_LOCAL:
+                    status.numSourceLocal++;
+                    break;
+                case SOURCE_POLL:
+                    status.numSourcePoll++;
+                    break;
+                case SOURCE_USER:
+                    status.numSourceUser++;
+                    break;
+                case SOURCE_SERVER:
+                    status.numSourceServer++;
+                    break;
+            }
+            
+            boolean writeStatisticsNow = false;
+            int day = getCurrentDay();
+            if (mDayStats[0] == null) {
+                mDayStats[0] = new DayStats(day);
+            } else if (day != mDayStats[0].day) {
+                System.arraycopy(mDayStats, 0, mDayStats, 1, mDayStats.length-1);
+                mDayStats[0] = new DayStats(day);
+                writeStatisticsNow = true;
+            } else if (mDayStats[0] == null) {
+            }
+            final DayStats ds = mDayStats[0];
+            
+            final long lastSyncTime = (item.eventTime + elapsedTime);
+            boolean writeStatusNow = false;
+            if (MESG_SUCCESS.equals(resultMessage)) {
+                // - if successful, update the successful columns
+                if (status.lastSuccessTime == 0 || status.lastFailureTime != 0) {
+                    writeStatusNow = true;
+                }
+                status.lastSuccessTime = lastSyncTime;
+                status.lastSuccessSource = item.source;
+                status.lastFailureTime = 0;
+                status.lastFailureSource = -1;
+                status.lastFailureMesg = null;
+                status.initialFailureTime = 0;
+                ds.successCount++;
+                ds.successTime += elapsedTime;
+            } else if (!MESG_CANCELED.equals(resultMessage)) {
+                if (status.lastFailureTime == 0) {
+                    writeStatusNow = true;
+                }
+                status.lastFailureTime = lastSyncTime;
+                status.lastFailureSource = item.source;
+                status.lastFailureMesg = resultMessage;
+                if (status.initialFailureTime == 0) {
+                    status.initialFailureTime = lastSyncTime;
+                }
+                ds.failureCount++;
+                ds.failureTime += elapsedTime;
+            }
+            
+            if (writeStatusNow) {
+                writeStatusLocked();
+            } else if (!hasMessages(MSG_WRITE_STATUS)) {
+                sendMessageDelayed(obtainMessage(MSG_WRITE_STATUS),
+                        WRITE_STATUS_DELAY);
+            }
+            if (writeStatisticsNow) {
+                writeStatisticsLocked();
+            } else if (!hasMessages(MSG_WRITE_STATISTICS)) {
+                sendMessageDelayed(obtainMessage(MSG_WRITE_STATISTICS),
+                        WRITE_STATISTICS_DELAY);
+            }            
+        }
+        
+        reportChange(CHANGE_STATUS);
+    }
+
+    /**
+     * Return the currently active sync information, or null if there is no
+     * active sync.  Note that the returned object is the real, live active
+     * sync object, so be careful what you do with it.
+     */
+    public ActiveSyncInfo getActiveSync() {
+        synchronized (mAuthorities) {
+            return mActiveSync;
+        }
+    }
+    
+    /**
+     * Return an array of the current sync status for all authorities.  Note
+     * that the objects inside the array are the real, live status objects,
+     * so be careful what you do with them.
+     */
+    public ArrayList<SyncStatusInfo> getSyncStatus() {
+        synchronized (mAuthorities) {
+            final int N = mSyncStatus.size();
+            ArrayList<SyncStatusInfo> ops = new ArrayList<SyncStatusInfo>(N);
+            for (int i=0; i<N; i++) {
+                ops.add(mSyncStatus.valueAt(i));
+            }
+            return ops;
+        }
+    }
+    
+    /**
+     * Returns the status that matches the authority. If there are multiples accounts for
+     * the authority, the one with the latest "lastSuccessTime" status is returned.
+     * @param authority the authority whose row should be selected
+     * @return the SyncStatusInfo for the authority, or null if none exists
+     */
+    public SyncStatusInfo getStatusByAuthority(String authority) {
+        synchronized (mAuthorities) {
+            SyncStatusInfo best = null;
+            final int N = mSyncStatus.size();
+            for (int i=0; i<N; i++) {
+                SyncStatusInfo cur = mSyncStatus.get(i);
+                AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
+                if (ainfo != null && ainfo.authority.equals(authority)) {
+                    if (best == null) {
+                        best = cur;
+                    } else if (best.lastSuccessTime > cur.lastSuccessTime) {
+                        best = cur;
+                    }
+                }
+            }
+            return best;
+        }
+    }
+    
+    /**
+     * Return true if the pending status is true of any matching authorities.
+     */
+    public boolean isAuthorityPending(Account account, String authority) {
+        synchronized (mAuthorities) {
+            final int N = mSyncStatus.size();
+            for (int i=0; i<N; i++) {
+                SyncStatusInfo cur = mSyncStatus.get(i);
+                AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
+                if (ainfo == null) {
+                    continue;
+                }
+                if (account != null && !ainfo.account.equals(account)) {
+                    continue;
+                }
+                if (ainfo.authority.equals(authority) && cur.pending) {
+                    return true;
+                }
+            }
+            return false;
         }
     }
 
     /**
+     * Return an array of the current sync status for all authorities.  Note
+     * that the objects inside the array are the real, live status objects,
+     * so be careful what you do with them.
+     */
+    public ArrayList<SyncHistoryItem> getSyncHistory() {
+        synchronized (mAuthorities) {
+            final int N = mSyncHistory.size();
+            ArrayList<SyncHistoryItem> items = new ArrayList<SyncHistoryItem>(N);
+            for (int i=0; i<N; i++) {
+                items.add(mSyncHistory.get(i));
+            }
+            return items;
+        }
+    }
+    
+    /**
+     * Return an array of the current per-day statistics.  Note
+     * that the objects inside the array are the real, live status objects,
+     * so be careful what you do with them.
+     */
+    public DayStats[] getDayStatistics() {
+        synchronized (mAuthorities) {
+            DayStats[] ds = new DayStats[mDayStats.length];
+            System.arraycopy(mDayStats, 0, ds, 0, ds.length);
+            return ds;
+        }
+    }
+    
+    /**
      * If sync is failing for any of the provider/accounts then determine the time at which it
      * started failing and return the earliest time over all the provider/accounts. If none are
      * failing then return 0.
      */
     public long getInitialSyncFailureTime() {
-        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
-        // Join the settings for a provider with the status so that we can easily
-        // check if each provider is enabled for syncing. We also join in the overall
-        // enabled flag ("listen_for_tickles") to each row so that we don't need to
-        // make a separate DB lookup to access it.
-        Cursor c = db.rawQuery(""
-                + "SELECT initialFailureTime, s1.value, s2.value "
-                + "FROM status "
-                + "LEFT JOIN stats ON status.stats_id=stats._id "
-                + "LEFT JOIN settings as s1 ON 'sync_provider_' || authority=s1.name "
-                + "LEFT JOIN settings as s2 ON s2.name='listen_for_tickles' "
-                + "where initialFailureTime is not null "
-                + "  AND lastFailureMesg!=" + Sync.History.ERROR_TOO_MANY_DELETIONS
-                + "  AND lastFailureMesg!=" + Sync.History.ERROR_AUTHENTICATION
-                + "  AND lastFailureMesg!=" + Sync.History.ERROR_SYNC_ALREADY_IN_PROGRESS
-                + "  AND authority!='subscribedfeeds' "
-                + " ORDER BY initialFailureTime", null);
+        synchronized (mAuthorities) {
+            if (!mListenForTickles) {
+                return 0;
+            }
+            
+            long oldest = 0;
+            int i = mSyncStatus.size();
+            while (i > 0) {
+                i--;
+                SyncStatusInfo stats = mSyncStatus.valueAt(i);
+                AuthorityInfo authority = mAuthorities.get(stats.authorityId);
+                if (authority != null && authority.enabled) {
+                    if (oldest == 0 || stats.initialFailureTime < oldest) {
+                        oldest = stats.initialFailureTime;
+                    }
+                }
+            }
+            
+            return oldest;
+        }
+    }
+    
+    private int getCurrentDay() {
+        mCal.setTimeInMillis(System.currentTimeMillis());
+        final int dayOfYear = mCal.get(Calendar.DAY_OF_YEAR);
+        if (mYear != mCal.get(Calendar.YEAR)) {
+            mYear = mCal.get(Calendar.YEAR);
+            mCal.clear();
+            mCal.set(Calendar.YEAR, mYear);
+            mYearInDays = (int)(mCal.getTimeInMillis()/86400000);
+        }
+        return dayOfYear + mYearInDays;
+    }
+    
+    /**
+     * Retrieve an authority, returning null if one does not exist.
+     * 
+     * @param accountName The name of the account for the authority.
+     * @param authorityName The name of the authority itself.
+     * @param tag If non-null, this will be used in a log message if the
+     * requested authority does not exist.
+     */
+    private AuthorityInfo getAuthorityLocked(Account accountName, String authorityName,
+            String tag) {
+        AccountInfo account = mAccounts.get(accountName);
+        if (account == null) {
+            if (tag != null) {
+                Log.w(TAG, tag + ": unknown account " + accountName);
+            }
+            return null;
+        }
+        AuthorityInfo authority = account.authorities.get(authorityName);
+        if (authority == null) {
+            if (tag != null) {
+                Log.w(TAG, tag + ": unknown authority " + authorityName);
+            }
+            return null;
+        }
+        
+        return authority;
+    }
+    
+    private AuthorityInfo getOrCreateAuthorityLocked(Account accountName,
+            String authorityName, int ident, boolean doWrite) {
+        AccountInfo account = mAccounts.get(accountName);
+        if (account == null) {
+            account = new AccountInfo(accountName);
+            mAccounts.put(accountName, account);
+        }
+        AuthorityInfo authority = account.authorities.get(authorityName);
+        if (authority == null) {
+            if (ident < 0) {
+                // Look for a new identifier for this authority.
+                final int N = mAuthorities.size();
+                ident = 0;
+                for (int i=0; i<N; i++) {
+                    if (mAuthorities.valueAt(i).ident > ident) {
+                        break;
+                    }
+                    ident++;
+                }
+            }
+            authority = new AuthorityInfo(accountName, authorityName, ident);
+            account.authorities.put(authorityName, authority);
+            mAuthorities.put(ident, authority);
+            if (doWrite) {
+                writeAccountInfoLocked();
+            }
+        }
+        
+        return authority;
+    }
+    
+    private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) {
+        SyncStatusInfo status = mSyncStatus.get(authorityId);
+        if (status == null) {
+            status = new SyncStatusInfo(authorityId);
+            mSyncStatus.put(authorityId, status);
+        }
+        return status;
+    }
+    
+    /**
+     * Read all account information back in to the initial engine state.
+     */
+    private void readAccountInfoLocked() {
+        FileInputStream fis = null;
         try {
+            fis = mAccountInfoFile.openRead();
+            if (DEBUG_FILE) Log.v(TAG, "Reading " + mAccountInfoFile.getBaseFile());
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(fis, null);
+            int eventType = parser.getEventType();
+            while (eventType != XmlPullParser.START_TAG) {
+                eventType = parser.next();
+            }
+            String tagName = parser.getName();
+            if ("accounts".equals(tagName)) {
+                String listen = parser.getAttributeValue(
+                        null, "listen-for-tickles");
+                mListenForTickles = listen == null
+                            || Boolean.parseBoolean(listen);
+                eventType = parser.next();
+                do {
+                    if (eventType == XmlPullParser.START_TAG
+                            && parser.getDepth() == 2) {
+                        tagName = parser.getName();
+                        if ("authority".equals(tagName)) {
+                            int id = -1;
+                            try {
+                                id = Integer.parseInt(parser.getAttributeValue(
+                                        null, "id"));
+                            } catch (NumberFormatException e) {
+                            } catch (NullPointerException e) {
+                            }
+                            if (id >= 0) {
+                                String accountName = parser.getAttributeValue(
+                                        null, "account");
+                                String accountType = parser.getAttributeValue(
+                                        null, "type");
+                                if (accountType == null) {
+                                    accountType = "com.google.GAIA";
+                                }
+                                String authorityName = parser.getAttributeValue(
+                                        null, "authority");
+                                String enabled = parser.getAttributeValue(
+                                        null, "enabled");
+                                AuthorityInfo authority = mAuthorities.get(id); 
+                                if (DEBUG_FILE) Log.v(TAG, "Adding authority: account="
+                                        + accountName + " auth=" + authorityName
+                                        + " enabled=" + enabled);
+                                if (authority == null) {
+                                    if (DEBUG_FILE) Log.v(TAG, "Creating entry");
+                                    authority = getOrCreateAuthorityLocked(
+                                            new Account(accountName, accountType),
+                                            authorityName, id, false);
+                                }
+                                if (authority != null) {
+                                    authority.enabled = enabled == null
+                                            || Boolean.parseBoolean(enabled);
+                                } else {
+                                    Log.w(TAG, "Failure adding authority: account="
+                                            + accountName + " auth=" + authorityName
+                                            + " enabled=" + enabled);
+                                }
+                            }
+                        }
+                    }
+                    eventType = parser.next();
+                } while (eventType != XmlPullParser.END_DOCUMENT);
+            }
+        } catch (XmlPullParserException e) {
+            Log.w(TAG, "Error reading accounts", e);
+        } catch (java.io.IOException e) {
+            if (fis == null) Log.i(TAG, "No initial accounts");
+            else Log.w(TAG, "Error reading accounts", e);
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (java.io.IOException e1) {
+                }
+            }
+        }
+    }
+    
+    /**
+     * Write all account information to the account file.
+     */
+    private void writeAccountInfoLocked() {
+        if (DEBUG_FILE) Log.v(TAG, "Writing new " + mAccountInfoFile.getBaseFile());
+        FileOutputStream fos = null;
+        
+        try {
+            fos = mAccountInfoFile.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, "accounts");
+            if (!mListenForTickles) {
+                out.attribute(null, "listen-for-tickles", "false");
+            }
+            
+            final int N = mAuthorities.size();
+            for (int i=0; i<N; i++) {
+                AuthorityInfo authority = mAuthorities.get(i);
+                out.startTag(null, "authority");
+                out.attribute(null, "id", Integer.toString(authority.ident));
+                out.attribute(null, "account", authority.account.mName);
+                out.attribute(null, "type", authority.account.mType);
+                out.attribute(null, "authority", authority.authority);
+                if (!authority.enabled) {
+                    out.attribute(null, "enabled", "false");
+                }
+                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);
+            if (fos != null) {
+                mAccountInfoFile.failWrite(fos);
+            }
+        }
+    }
+    
+    static int getIntColumn(Cursor c, String name) {
+        return c.getInt(c.getColumnIndex(name));
+    }
+    
+    static long getLongColumn(Cursor c, String name) {
+        return c.getLong(c.getColumnIndex(name));
+    }
+    
+    /**
+     * Load sync engine state from the old syncmanager database, and then
+     * erase it.  Note that we don't deal with pending operations, active
+     * sync, or history.
+     */
+    private void readLegacyAccountInfoLocked() {
+        // Look for old database to initialize from.
+        File file = mContext.getDatabasePath("syncmanager.db");
+        if (!file.exists()) {
+            return;
+        }
+        String path = file.getPath();
+        SQLiteDatabase db = null;
+        try {
+            db = SQLiteDatabase.openDatabase(path, null,
+                    SQLiteDatabase.OPEN_READONLY);
+        } catch (SQLiteException e) {
+        }
+        
+        if (db != null) {
+            // Copy in all of the status information, as well as accounts.
+            if (DEBUG_FILE) Log.v(TAG, "Reading legacy sync accounts db");
+            SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+            qb.setTables("stats, status");
+            HashMap<String,String> map = new HashMap<String,String>();
+            map.put("_id", "status._id as _id");
+            map.put("account", "stats.account as account");
+            map.put("type", "stats.type as type");
+            map.put("authority", "stats.authority as authority");
+            map.put("totalElapsedTime", "totalElapsedTime");
+            map.put("numSyncs", "numSyncs");
+            map.put("numSourceLocal", "numSourceLocal");
+            map.put("numSourcePoll", "numSourcePoll");
+            map.put("numSourceServer", "numSourceServer");
+            map.put("numSourceUser", "numSourceUser");
+            map.put("lastSuccessSource", "lastSuccessSource");
+            map.put("lastSuccessTime", "lastSuccessTime");
+            map.put("lastFailureSource", "lastFailureSource");
+            map.put("lastFailureTime", "lastFailureTime");
+            map.put("lastFailureMesg", "lastFailureMesg");
+            map.put("pending", "pending");
+            qb.setProjectionMap(map);
+            qb.appendWhere("stats._id = status.stats_id");
+            Cursor c = qb.query(db, null, null, null, null, null, null);
             while (c.moveToNext()) {
-                // these settings default to true, so if they are null treat them as enabled
-                final String providerEnabledString = c.getString(1);
-                if (providerEnabledString != null && !Boolean.parseBoolean(providerEnabledString)) {
-                    continue;
+                String accountName = c.getString(c.getColumnIndex("account"));
+                String accountType = c.getString(c.getColumnIndex("type"));
+                if (accountType == null) {
+                    accountType = "com.google.GAIA";
                 }
-                final String allEnabledString = c.getString(2);
-                if (allEnabledString != null && !Boolean.parseBoolean(allEnabledString)) {
-                    continue;
+                String authorityName = c.getString(c.getColumnIndex("authority"));
+                AuthorityInfo authority = this.getOrCreateAuthorityLocked(
+                        new Account(accountName, accountType),
+                        authorityName, -1, false);
+                if (authority != null) {
+                    int i = mSyncStatus.size();
+                    boolean found = false;
+                    SyncStatusInfo st = null;
+                    while (i > 0) {
+                        i--;
+                        st = mSyncStatus.get(i);
+                        if (st.authorityId == authority.ident) {
+                            found = true;
+                            break;
+                        }
+                    }
+                    if (!found) {
+                        st = new SyncStatusInfo(authority.ident);
+                        mSyncStatus.put(authority.ident, st);
+                    }
+                    st.totalElapsedTime = getLongColumn(c, "totalElapsedTime");
+                    st.numSyncs = getIntColumn(c, "numSyncs");
+                    st.numSourceLocal = getIntColumn(c, "numSourceLocal");
+                    st.numSourcePoll = getIntColumn(c, "numSourcePoll");
+                    st.numSourceServer = getIntColumn(c, "numSourceServer");
+                    st.numSourceUser = getIntColumn(c, "numSourceUser");
+                    st.lastSuccessSource = getIntColumn(c, "lastSuccessSource");
+                    st.lastSuccessTime = getLongColumn(c, "lastSuccessTime");
+                    st.lastFailureSource = getIntColumn(c, "lastFailureSource");
+                    st.lastFailureTime = getLongColumn(c, "lastFailureTime");
+                    st.lastFailureMesg = c.getString(c.getColumnIndex("lastFailureMesg"));
+                    st.pending = getIntColumn(c, "pending") != 0;
                 }
-                return c.getLong(0);
             }
-        } finally {
+            
             c.close();
-        }
-        return 0;
-    }
-
-    private void createStatusRowIfNecessary(long statsId) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        boolean statusExists = 0 != DatabaseUtils.longForQuery(db,
-                "SELECT count(*) FROM status WHERE stats_id=" + statsId, null);
-        if (!statusExists) {
-            ContentValues values = new ContentValues();
-            values.put("stats_id", statsId);
-            db.insert("status", null, values);
-        }
-    }
-
-    private long createStatsRowIfNecessary(Account account, String authority) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        StringBuilder where = new StringBuilder();
-        where.append(Sync.Stats.ACCOUNT + "= ?");
-        where.append(" and " + Sync.Stats.ACCOUNT_TYPE + "= ?");
-        where.append(" and " + Sync.Stats.AUTHORITY + "= ?");
-        Cursor cursor = query(Sync.Stats.CONTENT_URI,
-                Sync.Stats.SYNC_STATS_PROJECTION,
-                where.toString(), new String[] { account.mName, account.mType, authority },
-                null /* order */);
-        try {
-            long id;
-            if (cursor.moveToFirst()) {
-                id = cursor.getLong(cursor.getColumnIndexOrThrow(Sync.Stats._ID));
-            } else {
-                ContentValues values = new ContentValues();
-                values.put(Sync.Stats.ACCOUNT, account.mName);
-                values.put(Sync.Stats.ACCOUNT_TYPE, account.mType);
-                values.put(Sync.Stats.AUTHORITY, authority);
-                id = db.insert("stats", null, values);
+            
+            // Retrieve the settings.
+            qb = new SQLiteQueryBuilder();
+            qb.setTables("settings");
+            c = qb.query(db, null, null, null, null, null, null);
+            while (c.moveToNext()) {
+                String name = c.getString(c.getColumnIndex("name"));
+                String value = c.getString(c.getColumnIndex("value"));
+                if (name == null) continue;
+                if (name.equals("listen_for_tickles")) {
+                    setListenForNetworkTickles(value == null
+                            || Boolean.parseBoolean(value));
+                } else if (name.startsWith("sync_provider_")) {
+                    String provider = name.substring("sync_provider_".length(),
+                            name.length());
+                    setSyncProviderAutomatically(null, provider,
+                            value == null || Boolean.parseBoolean(value));
+                }
             }
-            return id;
+            
+            c.close();
+            
+            db.close();
+            
+            writeAccountInfoLocked();
+            writeStatusLocked();
+            (new File(path)).delete();
+        }
+    }
+    
+    public static final int STATUS_FILE_END = 0;
+    public static final int STATUS_FILE_ITEM = 100;
+    
+    /**
+     * Read all sync status back in to the initial engine state.
+     */
+    private void readStatusLocked() {
+        if (DEBUG_FILE) Log.v(TAG, "Reading " + mStatusFile.getBaseFile());
+        try {
+            byte[] data = mStatusFile.readFully();
+            Parcel in = Parcel.obtain();
+            in.unmarshall(data, 0, data.length);
+            in.setDataPosition(0);
+            int token;
+            while ((token=in.readInt()) != STATUS_FILE_END) {
+                if (token == STATUS_FILE_ITEM) {
+                    SyncStatusInfo status = new SyncStatusInfo(in);
+                    if (mAuthorities.indexOfKey(status.authorityId) >= 0) {
+                        status.pending = false;
+                        if (DEBUG_FILE) Log.v(TAG, "Adding status for id "
+                                + status.authorityId);
+                        mSyncStatus.put(status.authorityId, status);
+                    }
+                } else {
+                    // Ooops.
+                    Log.w(TAG, "Unknown status token: " + token);
+                    break;
+                }
+            }
+        } catch (java.io.IOException e) {
+            Log.i(TAG, "No initial status");
+        }
+    }
+    
+    /**
+     * Write all sync status to the sync status file.
+     */
+    private void writeStatusLocked() {
+        if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatusFile.getBaseFile());
+        
+        // The file is being written, so we don't need to have a scheduled
+        // write until the next change.
+        removeMessages(MSG_WRITE_STATUS);
+        
+        FileOutputStream fos = null;
+        try {
+            fos = mStatusFile.startWrite();
+            Parcel out = Parcel.obtain();
+            final int N = mSyncStatus.size();
+            for (int i=0; i<N; i++) {
+                SyncStatusInfo status = mSyncStatus.valueAt(i);
+                out.writeInt(STATUS_FILE_ITEM);
+                status.writeToParcel(out, 0);
+            }
+            out.writeInt(STATUS_FILE_END);
+            fos.write(out.marshall());
+            out.recycle();
+            
+            mStatusFile.finishWrite(fos);
+        } catch (java.io.IOException e1) {
+            Log.w(TAG, "Error writing status", e1);
+            if (fos != null) {
+                mStatusFile.failWrite(fos);
+            }
+        }
+    }
+    
+    public static final int PENDING_OPERATION_VERSION = 1;
+    
+    /**
+     * Read all pending operations back in to the initial engine state.
+     */
+    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) {
+                    Log.w(TAG, "Unknown pending operation version "
+                            + version + "; dropping all ops");
+                    break;
+                }
+                int authorityId = in.readInt();
+                int syncSource = in.readInt();
+                byte[] flatExtras = in.createByteArray();
+                AuthorityInfo authority = mAuthorities.get(authorityId);
+                if (authority != null) {
+                    Bundle extras = null;
+                    if (flatExtras != null) {
+                        extras = unflattenBundle(flatExtras);
+                    }
+                    PendingOperation op = new PendingOperation(
+                            authority.account, syncSource,
+                            authority.authority, extras);
+                    op.authorityId = authorityId;
+                    op.flatExtras = flatExtras;
+                    if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account
+                            + " auth=" + op.authority
+                            + " src=" + op.syncSource
+                            + " 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);
+    }
+    
+    /**
+     * 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 {
-            cursor.close();
+            try {
+                fos.close();
+            } catch (java.io.IOException e2) {
+            }
+        }
+    }
+    
+    static private byte[] flattenBundle(Bundle bundle) {
+        byte[] flatData = null;
+        Parcel parcel = Parcel.obtain();
+        try {
+            bundle.writeToParcel(parcel, 0);
+            flatData = parcel.marshall();
+        } finally {
+            parcel.recycle();
+        }
+        return flatData;
+    }
+    
+    static private Bundle unflattenBundle(byte[] flatData) {
+        Bundle bundle;
+        Parcel parcel = Parcel.obtain();
+        try {
+            parcel.unmarshall(flatData, 0, flatData.length);
+            parcel.setDataPosition(0);
+            bundle = parcel.readBundle();
+        } catch (RuntimeException e) {
+            // A RuntimeException is thrown if we were unable to parse the parcel.
+            // Create an empty parcel in this case.
+            bundle = new Bundle();
+        } finally {
+            parcel.recycle();
+        }
+        return bundle;
+    }
+    
+    public static final int STATISTICS_FILE_END = 0;
+    public static final int STATISTICS_FILE_ITEM_OLD = 100;
+    public static final int STATISTICS_FILE_ITEM = 101;
+    
+    /**
+     * Read all sync statistics back in to the initial engine state.
+     */
+    private void readStatisticsLocked() {
+        try {
+            byte[] data = mStatisticsFile.readFully();
+            Parcel in = Parcel.obtain();
+            in.unmarshall(data, 0, data.length);
+            in.setDataPosition(0);
+            int token;
+            int index = 0;
+            while ((token=in.readInt()) != STATISTICS_FILE_END) {
+                if (token == STATISTICS_FILE_ITEM
+                        || token == STATISTICS_FILE_ITEM_OLD) {
+                    int day = in.readInt();
+                    if (token == STATISTICS_FILE_ITEM_OLD) {
+                        day = day - 2009 + 14245;  // Magic!
+                    }
+                    DayStats ds = new DayStats(day);
+                    ds.successCount = in.readInt();
+                    ds.successTime = in.readLong();
+                    ds.failureCount = in.readInt();
+                    ds.failureTime = in.readLong();
+                    if (index < mDayStats.length) {
+                        mDayStats[index] = ds;
+                        index++;
+                    }
+                } else {
+                    // Ooops.
+                    Log.w(TAG, "Unknown stats token: " + token);
+                    break;
+                }
+            }
+        } catch (java.io.IOException e) {
+            Log.i(TAG, "No initial statistics");
+        }
+    }
+    
+    /**
+     * Write all sync statistics to the sync status file.
+     */
+    private void writeStatisticsLocked() {
+        if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatisticsFile.getBaseFile());
+        
+        // The file is being written, so we don't need to have a scheduled
+        // write until the next change.
+        removeMessages(MSG_WRITE_STATISTICS);
+        
+        FileOutputStream fos = null;
+        try {
+            fos = mStatisticsFile.startWrite();
+            Parcel out = Parcel.obtain();
+            final int N = mDayStats.length;
+            for (int i=0; i<N; i++) {
+                DayStats ds = mDayStats[i];
+                if (ds == null) {
+                    break;
+                }
+                out.writeInt(STATISTICS_FILE_ITEM);
+                out.writeInt(ds.day);
+                out.writeInt(ds.successCount);
+                out.writeLong(ds.successTime);
+                out.writeInt(ds.failureCount);
+                out.writeLong(ds.failureTime);
+            }
+            out.writeInt(STATISTICS_FILE_END);
+            fos.write(out.marshall());
+            out.recycle();
+            
+            mStatisticsFile.finishWrite(fos);
+        } catch (java.io.IOException e1) {
+            Log.w(TAG, "Error writing stats", e1);
+            if (fos != null) {
+                mStatisticsFile.failWrite(fos);
+            }
         }
     }
 }