summaryrefslogtreecommitdiffstats
path: root/core/java/android/content/SyncStorageEngine.java
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
commit9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch)
treed88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/content/SyncStorageEngine.java
parentd83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff)
downloadframeworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/content/SyncStorageEngine.java')
-rw-r--r--core/java/android/content/SyncStorageEngine.java758
1 files changed, 758 insertions, 0 deletions
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();
+ }
+ }
+}