auto import from //depot/cupcake/@135843
diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java
new file mode 100644
index 0000000..282f6e7
--- /dev/null
+++ b/core/java/android/content/SyncStorageEngine.java
@@ -0,0 +1,758 @@
+package android.content;
+
+import android.Manifest;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.provider.Sync;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * ContentProvider that tracks the sync data and overall sync
+ * history on the device.
+ * 
+ * @hide
+ */
+public class SyncStorageEngine {
+    private static final String TAG = "SyncManager";
+
+    private static final String DATABASE_NAME = "syncmanager.db";
+    private static final int DATABASE_VERSION = 10;
+
+    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;
+
+    private static final UriMatcher sURLMatcher =
+            new UriMatcher(UriMatcher.NO_MATCH);
+
+    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;
+
+    private final Context mContext;
+    private final SQLiteOpenHelper mOpenHelper;
+    private static SyncStorageEngine sSyncStorageEngine = null;
+
+    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);
+
+        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.AUTHORITY, Sync.History.AUTHORITY);
+
+        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.AUTHORITY, Sync.History.AUTHORITY);
+        map.put("startTime", "startTime");
+
+        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.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);
+
+        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.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);
+    }
+
+    private static final String[] STATS_ACCOUNT_PROJECTION =
+            new String[] { Sync.Stats.ACCOUNT };
+
+    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 SyncStorageEngine(Context context) {
+        mContext = context;
+        mOpenHelper = new SyncStorageEngine.DatabaseHelper(context);
+        sSyncStorageEngine = this;
+    }
+
+    public static SyncStorageEngine newTestInstance(Context context) {
+        return new SyncStorageEngine(context);
+    }
+
+    public static void init(Context context) {
+        if (sSyncStorageEngine != null) {
+            throw new IllegalStateException("already initialized");
+        }
+        sSyncStorageEngine = new SyncStorageEngine(context);
+    }
+
+    public static SyncStorageEngine getSingleton() {
+        if (sSyncStorageEngine == null) {
+            throw new IllegalStateException("not initialized");
+        }
+        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,"
+                    + "extras BLOB NOT NULL,"
+                    + "source INTEGER NOT NULL"
+                    + ");");
+
+            db.execSQL("CREATE TABLE stats (" +
+                       "_id INTEGER PRIMARY KEY," +
+                       "account 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,"
+                    + "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 && newVersion == 10) {
+                Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+                        + newVersion + ", which will preserve old data");
+                db.execSQL("ALTER TABLE status ADD COLUMN initialFailureTime INTEGER");
+                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);
+            }
+        }
+    }
+
+    protected void doDatabaseCleanup(String[] accounts) {
+        HashSet<String> currentAccounts = new HashSet<String>();
+        for (String account : accounts) currentAccounts.add(account);
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        Cursor cursor = db.query("stats", STATS_ACCOUNT_PROJECTION,
+                null /* where */, null /* where args */, Sync.Stats.ACCOUNT,
+                null /* having */, null /* order by */);
+        try {
+            while (cursor.moveToNext()) {
+                String account = cursor.getString(0);
+                if (TextUtils.isEmpty(account)) {
+                    continue;
+                }
+                if (!currentAccounts.contains(account)) {
+                    String where = Sync.Stats.ACCOUNT + "=?";
+                    int numDeleted;
+                    numDeleted = db.delete("stats", where, new String[]{account});
+                    if (Config.LOGD) {
+                        Log.d(TAG, "deleted " + numDeleted
+                                + " records from stats table"
+                                + " for account " + account);
+                    }
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    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(String account, String authority, Long startTime) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        ContentValues values = new ContentValues();
+        values.put("account", account);
+        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 account = values.getAsString(Sync.Pending.ACCOUNT);
+            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 {
+            String account;
+            String authority;
+            Cursor c = db.query("pending",
+                    new String[]{Sync.Pending.ACCOUNT, Sync.Pending.AUTHORITY},
+                    "_id=" + rowId, null, null, null, null);
+            try {
+                if (c.getCount() != 1) {
+                    return 0;
+                }
+                c.moveToNext();
+                account = c.getString(0);
+                authority = c.getString(1);
+            } finally {
+                c.close();
+            }
+            db.delete("pending", "_id=" + rowId, null /* no where args */);
+            final String[] accountAuthorityWhereArgs = new String[]{account, authority};
+            boolean isPending = 0 < DatabaseUtils.longForQuery(db,
+                    "SELECT COUNT(*) FROM PENDING WHERE account=? 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;
+    }
+
+    public long insertStartSyncEvent(String account, String authority, long now, int source) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        long statsId = createStatsRowIfNecessary(account, authority);
+
+        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 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;
+                }
+
+                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 */);
+            }
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    /**
+     * 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);
+        try {
+            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;
+                }
+                final String allEnabledString = c.getString(2);
+                if (allEnabledString != null && !Boolean.parseBoolean(allEnabledString)) {
+                    continue;
+                }
+                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(String account, String authority) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        StringBuilder where = new StringBuilder();
+        where.append(Sync.Stats.ACCOUNT + "= ?");
+        where.append(" and " + Sync.Stats.AUTHORITY + "= ?");
+        Cursor cursor = query(Sync.Stats.CONTENT_URI,
+                Sync.Stats.SYNC_STATS_PROJECTION,
+                where.toString(), new String[] { account, 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);
+                values.put(Sync.Stats.AUTHORITY, authority);
+                id = db.insert("stats", null, values);
+            }
+            return id;
+        } finally {
+            cursor.close();
+        }
+    }
+}