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(); } } }