blob: 282f6e71db7053a3d2937959f68c89ee9fa8d016 [file] [log] [blame]
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();
}
}
}