blob: 282f6e71db7053a3d2937959f68c89ee9fa8d016 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001package android.content;
2
3import android.Manifest;
4import android.database.Cursor;
5import android.database.DatabaseUtils;
6import android.database.sqlite.SQLiteDatabase;
7import android.database.sqlite.SQLiteOpenHelper;
8import android.database.sqlite.SQLiteQueryBuilder;
9import android.net.Uri;
10import android.provider.Sync;
11import android.text.TextUtils;
12import android.util.Config;
13import android.util.Log;
14
15import java.util.ArrayList;
16import java.util.HashMap;
17import java.util.HashSet;
18
19/**
20 * ContentProvider that tracks the sync data and overall sync
21 * history on the device.
22 *
23 * @hide
24 */
25public class SyncStorageEngine {
26 private static final String TAG = "SyncManager";
27
28 private static final String DATABASE_NAME = "syncmanager.db";
29 private static final int DATABASE_VERSION = 10;
30
31 private static final int STATS = 1;
32 private static final int STATS_ID = 2;
33 private static final int HISTORY = 3;
34 private static final int HISTORY_ID = 4;
35 private static final int SETTINGS = 5;
36 private static final int PENDING = 7;
37 private static final int ACTIVE = 8;
38 private static final int STATUS = 9;
39
40 private static final UriMatcher sURLMatcher =
41 new UriMatcher(UriMatcher.NO_MATCH);
42
43 private static final HashMap<String,String> HISTORY_PROJECTION_MAP;
44 private static final HashMap<String,String> PENDING_PROJECTION_MAP;
45 private static final HashMap<String,String> ACTIVE_PROJECTION_MAP;
46 private static final HashMap<String,String> STATUS_PROJECTION_MAP;
47
48 private final Context mContext;
49 private final SQLiteOpenHelper mOpenHelper;
50 private static SyncStorageEngine sSyncStorageEngine = null;
51
52 static {
53 sURLMatcher.addURI("sync", "stats", STATS);
54 sURLMatcher.addURI("sync", "stats/#", STATS_ID);
55 sURLMatcher.addURI("sync", "history", HISTORY);
56 sURLMatcher.addURI("sync", "history/#", HISTORY_ID);
57 sURLMatcher.addURI("sync", "settings", SETTINGS);
58 sURLMatcher.addURI("sync", "status", STATUS);
59 sURLMatcher.addURI("sync", "active", ACTIVE);
60 sURLMatcher.addURI("sync", "pending", PENDING);
61
62 HashMap<String,String> map;
63 PENDING_PROJECTION_MAP = map = new HashMap<String,String>();
64 map.put(Sync.History._ID, Sync.History._ID);
65 map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT);
66 map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY);
67
68 ACTIVE_PROJECTION_MAP = map = new HashMap<String,String>();
69 map.put(Sync.History._ID, Sync.History._ID);
70 map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT);
71 map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY);
72 map.put("startTime", "startTime");
73
74 HISTORY_PROJECTION_MAP = map = new HashMap<String,String>();
75 map.put(Sync.History._ID, "history._id as _id");
76 map.put(Sync.History.ACCOUNT, "stats.account as account");
77 map.put(Sync.History.AUTHORITY, "stats.authority as authority");
78 map.put(Sync.History.EVENT, Sync.History.EVENT);
79 map.put(Sync.History.EVENT_TIME, Sync.History.EVENT_TIME);
80 map.put(Sync.History.ELAPSED_TIME, Sync.History.ELAPSED_TIME);
81 map.put(Sync.History.SOURCE, Sync.History.SOURCE);
82 map.put(Sync.History.UPSTREAM_ACTIVITY, Sync.History.UPSTREAM_ACTIVITY);
83 map.put(Sync.History.DOWNSTREAM_ACTIVITY, Sync.History.DOWNSTREAM_ACTIVITY);
84 map.put(Sync.History.MESG, Sync.History.MESG);
85
86 STATUS_PROJECTION_MAP = map = new HashMap<String,String>();
87 map.put(Sync.Status._ID, "status._id as _id");
88 map.put(Sync.Status.ACCOUNT, "stats.account as account");
89 map.put(Sync.Status.AUTHORITY, "stats.authority as authority");
90 map.put(Sync.Status.TOTAL_ELAPSED_TIME, Sync.Status.TOTAL_ELAPSED_TIME);
91 map.put(Sync.Status.NUM_SYNCS, Sync.Status.NUM_SYNCS);
92 map.put(Sync.Status.NUM_SOURCE_LOCAL, Sync.Status.NUM_SOURCE_LOCAL);
93 map.put(Sync.Status.NUM_SOURCE_POLL, Sync.Status.NUM_SOURCE_POLL);
94 map.put(Sync.Status.NUM_SOURCE_SERVER, Sync.Status.NUM_SOURCE_SERVER);
95 map.put(Sync.Status.NUM_SOURCE_USER, Sync.Status.NUM_SOURCE_USER);
96 map.put(Sync.Status.LAST_SUCCESS_SOURCE, Sync.Status.LAST_SUCCESS_SOURCE);
97 map.put(Sync.Status.LAST_SUCCESS_TIME, Sync.Status.LAST_SUCCESS_TIME);
98 map.put(Sync.Status.LAST_FAILURE_SOURCE, Sync.Status.LAST_FAILURE_SOURCE);
99 map.put(Sync.Status.LAST_FAILURE_TIME, Sync.Status.LAST_FAILURE_TIME);
100 map.put(Sync.Status.LAST_FAILURE_MESG, Sync.Status.LAST_FAILURE_MESG);
101 map.put(Sync.Status.PENDING, Sync.Status.PENDING);
102 }
103
104 private static final String[] STATS_ACCOUNT_PROJECTION =
105 new String[] { Sync.Stats.ACCOUNT };
106
107 private static final int MAX_HISTORY_EVENTS_TO_KEEP = 5000;
108
109 private static final String SELECT_INITIAL_FAILURE_TIME_QUERY_STRING = ""
110 + "SELECT min(a) "
111 + "FROM ("
112 + " SELECT initialFailureTime AS a "
113 + " FROM status "
114 + " WHERE stats_id=? AND a IS NOT NULL "
115 + " UNION "
116 + " SELECT ? AS a"
117 + " )";
118
119 private SyncStorageEngine(Context context) {
120 mContext = context;
121 mOpenHelper = new SyncStorageEngine.DatabaseHelper(context);
122 sSyncStorageEngine = this;
123 }
124
125 public static SyncStorageEngine newTestInstance(Context context) {
126 return new SyncStorageEngine(context);
127 }
128
129 public static void init(Context context) {
130 if (sSyncStorageEngine != null) {
131 throw new IllegalStateException("already initialized");
132 }
133 sSyncStorageEngine = new SyncStorageEngine(context);
134 }
135
136 public static SyncStorageEngine getSingleton() {
137 if (sSyncStorageEngine == null) {
138 throw new IllegalStateException("not initialized");
139 }
140 return sSyncStorageEngine;
141 }
142
143 private class DatabaseHelper extends SQLiteOpenHelper {
144 DatabaseHelper(Context context) {
145 super(context, DATABASE_NAME, null, DATABASE_VERSION);
146 }
147
148 @Override
149 public void onCreate(SQLiteDatabase db) {
150 db.execSQL("CREATE TABLE pending ("
151 + "_id INTEGER PRIMARY KEY,"
152 + "authority TEXT NOT NULL,"
153 + "account TEXT NOT NULL,"
154 + "extras BLOB NOT NULL,"
155 + "source INTEGER NOT NULL"
156 + ");");
157
158 db.execSQL("CREATE TABLE stats (" +
159 "_id INTEGER PRIMARY KEY," +
160 "account TEXT, " +
161 "authority TEXT, " +
162 "syncdata TEXT, " +
163 "UNIQUE (account, authority)" +
164 ");");
165
166 db.execSQL("CREATE TABLE history (" +
167 "_id INTEGER PRIMARY KEY," +
168 "stats_id INTEGER," +
169 "eventTime INTEGER," +
170 "elapsedTime INTEGER," +
171 "source INTEGER," +
172 "event INTEGER," +
173 "upstreamActivity INTEGER," +
174 "downstreamActivity INTEGER," +
175 "mesg TEXT);");
176
177 db.execSQL("CREATE TABLE status ("
178 + "_id INTEGER PRIMARY KEY,"
179 + "stats_id INTEGER NOT NULL,"
180 + "totalElapsedTime INTEGER NOT NULL DEFAULT 0,"
181 + "numSyncs INTEGER NOT NULL DEFAULT 0,"
182 + "numSourcePoll INTEGER NOT NULL DEFAULT 0,"
183 + "numSourceServer INTEGER NOT NULL DEFAULT 0,"
184 + "numSourceLocal INTEGER NOT NULL DEFAULT 0,"
185 + "numSourceUser INTEGER NOT NULL DEFAULT 0,"
186 + "lastSuccessTime INTEGER,"
187 + "lastSuccessSource INTEGER,"
188 + "lastFailureTime INTEGER,"
189 + "lastFailureSource INTEGER,"
190 + "lastFailureMesg STRING,"
191 + "initialFailureTime INTEGER,"
192 + "pending INTEGER NOT NULL DEFAULT 0);");
193
194 db.execSQL("CREATE TABLE active ("
195 + "_id INTEGER PRIMARY KEY,"
196 + "authority TEXT,"
197 + "account TEXT,"
198 + "startTime INTEGER);");
199
200 db.execSQL("CREATE INDEX historyEventTime ON history (eventTime)");
201
202 db.execSQL("CREATE TABLE settings (" +
203 "name TEXT PRIMARY KEY," +
204 "value TEXT);");
205 }
206
207 @Override
208 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
209 if (oldVersion == 9 && newVersion == 10) {
210 Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
211 + newVersion + ", which will preserve old data");
212 db.execSQL("ALTER TABLE status ADD COLUMN initialFailureTime INTEGER");
213 return;
214 }
215
216 Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
217 + newVersion + ", which will destroy all old data");
218 db.execSQL("DROP TABLE IF EXISTS pending");
219 db.execSQL("DROP TABLE IF EXISTS stats");
220 db.execSQL("DROP TABLE IF EXISTS history");
221 db.execSQL("DROP TABLE IF EXISTS settings");
222 db.execSQL("DROP TABLE IF EXISTS active");
223 db.execSQL("DROP TABLE IF EXISTS status");
224 onCreate(db);
225 }
226
227 @Override
228 public void onOpen(SQLiteDatabase db) {
229 if (!db.isReadOnly()) {
230 db.delete("active", null, null);
231 db.insert("active", "account", null);
232 }
233 }
234 }
235
236 protected void doDatabaseCleanup(String[] accounts) {
237 HashSet<String> currentAccounts = new HashSet<String>();
238 for (String account : accounts) currentAccounts.add(account);
239 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
240 Cursor cursor = db.query("stats", STATS_ACCOUNT_PROJECTION,
241 null /* where */, null /* where args */, Sync.Stats.ACCOUNT,
242 null /* having */, null /* order by */);
243 try {
244 while (cursor.moveToNext()) {
245 String account = cursor.getString(0);
246 if (TextUtils.isEmpty(account)) {
247 continue;
248 }
249 if (!currentAccounts.contains(account)) {
250 String where = Sync.Stats.ACCOUNT + "=?";
251 int numDeleted;
252 numDeleted = db.delete("stats", where, new String[]{account});
253 if (Config.LOGD) {
254 Log.d(TAG, "deleted " + numDeleted
255 + " records from stats table"
256 + " for account " + account);
257 }
258 }
259 }
260 } finally {
261 cursor.close();
262 }
263 }
264
265 protected void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
266 if (activeSyncContext != null) {
267 updateActiveSync(activeSyncContext.mSyncOperation.account,
268 activeSyncContext.mSyncOperation.authority, activeSyncContext.mStartTime);
269 } else {
270 // we indicate that the sync is not active by passing null for all the parameters
271 updateActiveSync(null, null, null);
272 }
273 }
274
275 private int updateActiveSync(String account, String authority, Long startTime) {
276 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
277 ContentValues values = new ContentValues();
278 values.put("account", account);
279 values.put("authority", authority);
280 values.put("startTime", startTime);
281 int numChanges = db.update("active", values, null, null);
282 if (numChanges > 0) {
283 mContext.getContentResolver().notifyChange(Sync.Active.CONTENT_URI,
284 null /* this change wasn't made through an observer */);
285 }
286 return numChanges;
287 }
288
289 /**
290 * Implements the {@link ContentProvider#query} method
291 */
292 public Cursor query(Uri url, String[] projectionIn,
293 String selection, String[] selectionArgs, String sort) {
294 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
295
296 // Generate the body of the query
297 int match = sURLMatcher.match(url);
298 String groupBy = null;
299 switch (match) {
300 case STATS:
301 qb.setTables("stats");
302 break;
303 case STATS_ID:
304 qb.setTables("stats");
305 qb.appendWhere("_id=");
306 qb.appendWhere(url.getPathSegments().get(1));
307 break;
308 case HISTORY:
309 // join the stats and history tables, so the caller can get
310 // the account and authority information as part of this query.
311 qb.setTables("stats, history");
312 qb.setProjectionMap(HISTORY_PROJECTION_MAP);
313 qb.appendWhere("stats._id = history.stats_id");
314 break;
315 case ACTIVE:
316 qb.setTables("active");
317 qb.setProjectionMap(ACTIVE_PROJECTION_MAP);
318 qb.appendWhere("account is not null");
319 break;
320 case PENDING:
321 qb.setTables("pending");
322 qb.setProjectionMap(PENDING_PROJECTION_MAP);
323 groupBy = "account, authority";
324 break;
325 case STATUS:
326 // join the stats and status tables, so the caller can get
327 // the account and authority information as part of this query.
328 qb.setTables("stats, status");
329 qb.setProjectionMap(STATUS_PROJECTION_MAP);
330 qb.appendWhere("stats._id = status.stats_id");
331 break;
332 case HISTORY_ID:
333 // join the stats and history tables, so the caller can get
334 // the account and authority information as part of this query.
335 qb.setTables("stats, history");
336 qb.setProjectionMap(HISTORY_PROJECTION_MAP);
337 qb.appendWhere("stats._id = history.stats_id");
338 qb.appendWhere("AND history._id=");
339 qb.appendWhere(url.getPathSegments().get(1));
340 break;
341 case SETTINGS:
342 qb.setTables("settings");
343 break;
344 default:
345 throw new IllegalArgumentException("Unknown URL " + url);
346 }
347
348 if (match == SETTINGS) {
349 mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
350 "no permission to read the sync settings");
351 } else {
352 mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
353 "no permission to read the sync stats");
354 }
355 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
356 Cursor c = qb.query(db, projectionIn, selection, selectionArgs, groupBy, null, sort);
357 c.setNotificationUri(mContext.getContentResolver(), url);
358 return c;
359 }
360
361 /**
362 * Implements the {@link ContentProvider#insert} method
363 * @param callerIsTheProvider true if this is being called via the
364 * {@link ContentProvider#insert} in method rather than directly.
365 * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
366 * for the Settings table.
367 */
368 public Uri insert(boolean callerIsTheProvider, Uri url, ContentValues values) {
369 String table;
370 long rowID;
371 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
372 final int match = sURLMatcher.match(url);
373 checkCaller(callerIsTheProvider, match);
374 switch (match) {
375 case SETTINGS:
376 mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
377 "no permission to write the sync settings");
378 table = "settings";
379 rowID = db.replace(table, null, values);
380 break;
381 default:
382 throw new IllegalArgumentException("Unknown URL " + url);
383 }
384
385
386 if (rowID > 0) {
387 mContext.getContentResolver().notifyChange(url, null /* observer */);
388 return Uri.parse("content://sync/" + table + "/" + rowID);
389 }
390
391 return null;
392 }
393
394 private static void checkCaller(boolean callerIsTheProvider, int match) {
395 if (callerIsTheProvider && match != SETTINGS) {
396 throw new UnsupportedOperationException(
397 "only the settings are modifiable via the ContentProvider interface");
398 }
399 }
400
401 /**
402 * Implements the {@link ContentProvider#delete} method
403 * @param callerIsTheProvider true if this is being called via the
404 * {@link ContentProvider#delete} in method rather than directly.
405 * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
406 * for the Settings table.
407 */
408 public int delete(boolean callerIsTheProvider, Uri url, String where, String[] whereArgs) {
409 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
410 int match = sURLMatcher.match(url);
411
412 int numRows;
413 switch (match) {
414 case SETTINGS:
415 mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
416 "no permission to write the sync settings");
417 numRows = db.delete("settings", where, whereArgs);
418 break;
419 default:
420 throw new UnsupportedOperationException("Cannot delete URL: " + url);
421 }
422
423 if (numRows > 0) {
424 mContext.getContentResolver().notifyChange(url, null /* observer */);
425 }
426 return numRows;
427 }
428
429 /**
430 * Implements the {@link ContentProvider#update} method
431 * @param callerIsTheProvider true if this is being called via the
432 * {@link ContentProvider#update} in method rather than directly.
433 * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
434 * for the Settings table.
435 */
436 public int update(boolean callerIsTheProvider, Uri url, ContentValues initialValues,
437 String where, String[] whereArgs) {
438 switch (sURLMatcher.match(url)) {
439 case SETTINGS:
440 throw new UnsupportedOperationException("updating url " + url
441 + " is not allowed, use insert instead");
442 default:
443 throw new UnsupportedOperationException("Cannot update URL: " + url);
444 }
445 }
446
447 /**
448 * Implements the {@link ContentProvider#getType} method
449 */
450 public String getType(Uri url) {
451 int match = sURLMatcher.match(url);
452 switch (match) {
453 case SETTINGS:
454 return "vnd.android.cursor.dir/sync-settings";
455 default:
456 throw new IllegalArgumentException("Unknown URL");
457 }
458 }
459
460 protected Uri insertIntoPending(ContentValues values) {
461 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
462 try {
463 db.beginTransaction();
464 long rowId = db.insert("pending", Sync.Pending.ACCOUNT, values);
465 if (rowId < 0) return null;
466 String account = values.getAsString(Sync.Pending.ACCOUNT);
467 String authority = values.getAsString(Sync.Pending.AUTHORITY);
468
469 long statsId = createStatsRowIfNecessary(account, authority);
470 createStatusRowIfNecessary(statsId);
471
472 values.clear();
473 values.put(Sync.Status.PENDING, 1);
474 int numUpdatesStatus = db.update("status", values, "stats_id=" + statsId, null);
475
476 db.setTransactionSuccessful();
477
478 mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
479 null /* no observer initiated this change */);
480 if (numUpdatesStatus > 0) {
481 mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
482 null /* no observer initiated this change */);
483 }
484 return ContentUris.withAppendedId(Sync.Pending.CONTENT_URI, rowId);
485 } finally {
486 db.endTransaction();
487 }
488 }
489
490 int deleteFromPending(long rowId) {
491 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
492 db.beginTransaction();
493 try {
494 String account;
495 String authority;
496 Cursor c = db.query("pending",
497 new String[]{Sync.Pending.ACCOUNT, Sync.Pending.AUTHORITY},
498 "_id=" + rowId, null, null, null, null);
499 try {
500 if (c.getCount() != 1) {
501 return 0;
502 }
503 c.moveToNext();
504 account = c.getString(0);
505 authority = c.getString(1);
506 } finally {
507 c.close();
508 }
509 db.delete("pending", "_id=" + rowId, null /* no where args */);
510 final String[] accountAuthorityWhereArgs = new String[]{account, authority};
511 boolean isPending = 0 < DatabaseUtils.longForQuery(db,
512 "SELECT COUNT(*) FROM PENDING WHERE account=? AND authority=?",
513 accountAuthorityWhereArgs);
514 if (!isPending) {
515 long statsId = createStatsRowIfNecessary(account, authority);
516 db.execSQL("UPDATE status SET pending=0 WHERE stats_id=" + statsId);
517 }
518 db.setTransactionSuccessful();
519
520 mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
521 null /* no observer initiated this change */);
522 if (!isPending) {
523 mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
524 null /* no observer initiated this change */);
525 }
526 return 1;
527 } finally {
528 db.endTransaction();
529 }
530 }
531
532 int clearPending() {
533 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
534 db.beginTransaction();
535 try {
536 int numChanges = db.delete("pending", null, null /* no where args */);
537 if (numChanges > 0) {
538 db.execSQL("UPDATE status SET pending=0");
539 mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
540 null /* no observer initiated this change */);
541 mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
542 null /* no observer initiated this change */);
543 }
544 db.setTransactionSuccessful();
545 return numChanges;
546 } finally {
547 db.endTransaction();
548 }
549 }
550
551 /**
552 * Returns a cursor over all the pending syncs in no particular order. This cursor is not
553 * "live", in that if changes are made to the pending table any observers on this cursor
554 * will not be notified.
555 * @param projection Return only these columns. If null then all columns are returned.
556 * @return the cursor of pending syncs
557 */
558 public Cursor getPendingSyncsCursor(String[] projection) {
559 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
560 return db.query("pending", projection, null, null, null, null, null);
561 }
562
563 // @VisibleForTesting
564 static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
565
566 private boolean purgeOldHistoryEvents(long now) {
567 // remove events that are older than MILLIS_IN_4WEEKS
568 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
569 int numDeletes = db.delete("history", "eventTime<" + (now - MILLIS_IN_4WEEKS), null);
570 if (Log.isLoggable(TAG, Log.VERBOSE)) {
571 if (numDeletes > 0) {
572 Log.v(TAG, "deleted " + numDeletes + " old event(s) from the sync history");
573 }
574 }
575
576 // keep only the last MAX_HISTORY_EVENTS_TO_KEEP history events
577 numDeletes += db.delete("history", "eventTime < (select min(eventTime) from "
578 + "(select eventTime from history order by eventTime desc limit ?))",
579 new String[]{String.valueOf(MAX_HISTORY_EVENTS_TO_KEEP)});
580
581 return numDeletes > 0;
582 }
583
584 public long insertStartSyncEvent(String account, String authority, long now, int source) {
585 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
586 long statsId = createStatsRowIfNecessary(account, authority);
587
588 purgeOldHistoryEvents(now);
589 ContentValues values = new ContentValues();
590 values.put(Sync.History.STATS_ID, statsId);
591 values.put(Sync.History.EVENT_TIME, now);
592 values.put(Sync.History.SOURCE, source);
593 values.put(Sync.History.EVENT, Sync.History.EVENT_START);
594 long rowId = db.insert("history", null, values);
595 mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI, null /* observer */);
596 mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, null /* observer */);
597 return rowId;
598 }
599
600 public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage,
601 long downstreamActivity, long upstreamActivity) {
602 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
603 db.beginTransaction();
604 try {
605 ContentValues values = new ContentValues();
606 values.put(Sync.History.ELAPSED_TIME, elapsedTime);
607 values.put(Sync.History.EVENT, Sync.History.EVENT_STOP);
608 values.put(Sync.History.MESG, resultMessage);
609 values.put(Sync.History.DOWNSTREAM_ACTIVITY, downstreamActivity);
610 values.put(Sync.History.UPSTREAM_ACTIVITY, upstreamActivity);
611
612 int count = db.update("history", values, "_id=?",
613 new String[]{Long.toString(historyId)});
614 // We think that count should always be 1 but don't want to change this until after
615 // launch.
616 if (count > 0) {
617 int source = (int) DatabaseUtils.longForQuery(db,
618 "SELECT source FROM history WHERE _id=" + historyId, null);
619 long eventTime = DatabaseUtils.longForQuery(db,
620 "SELECT eventTime FROM history WHERE _id=" + historyId, null);
621 long statsId = DatabaseUtils.longForQuery(db,
622 "SELECT stats_id FROM history WHERE _id=" + historyId, null);
623
624 createStatusRowIfNecessary(statsId);
625
626 // update the status table to reflect this sync
627 StringBuilder sb = new StringBuilder();
628 ArrayList<String> bindArgs = new ArrayList<String>();
629 sb.append("UPDATE status SET");
630 sb.append(" numSyncs=numSyncs+1");
631 sb.append(", totalElapsedTime=totalElapsedTime+" + elapsedTime);
632 switch (source) {
633 case Sync.History.SOURCE_LOCAL:
634 sb.append(", numSourceLocal=numSourceLocal+1");
635 break;
636 case Sync.History.SOURCE_POLL:
637 sb.append(", numSourcePoll=numSourcePoll+1");
638 break;
639 case Sync.History.SOURCE_USER:
640 sb.append(", numSourceUser=numSourceUser+1");
641 break;
642 case Sync.History.SOURCE_SERVER:
643 sb.append(", numSourceServer=numSourceServer+1");
644 break;
645 }
646
647 final String statsIdString = String.valueOf(statsId);
648 final long lastSyncTime = (eventTime + elapsedTime);
649 if (Sync.History.MESG_SUCCESS.equals(resultMessage)) {
650 // - if successful, update the successful columns
651 sb.append(", lastSuccessTime=" + lastSyncTime);
652 sb.append(", lastSuccessSource=" + source);
653 sb.append(", lastFailureTime=null");
654 sb.append(", lastFailureSource=null");
655 sb.append(", lastFailureMesg=null");
656 sb.append(", initialFailureTime=null");
657 } else if (!Sync.History.MESG_CANCELED.equals(resultMessage)) {
658 sb.append(", lastFailureTime=" + lastSyncTime);
659 sb.append(", lastFailureSource=" + source);
660 sb.append(", lastFailureMesg=?");
661 bindArgs.add(resultMessage);
662 long initialFailureTime = DatabaseUtils.longForQuery(db,
663 SELECT_INITIAL_FAILURE_TIME_QUERY_STRING,
664 new String[]{statsIdString, String.valueOf(lastSyncTime)});
665 sb.append(", initialFailureTime=" + initialFailureTime);
666 }
667 sb.append(" WHERE stats_id=?");
668 bindArgs.add(statsIdString);
669 db.execSQL(sb.toString(), bindArgs.toArray());
670 db.setTransactionSuccessful();
671 mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI,
672 null /* observer */);
673 mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
674 null /* observer */);
675 }
676 } finally {
677 db.endTransaction();
678 }
679 }
680
681 /**
682 * If sync is failing for any of the provider/accounts then determine the time at which it
683 * started failing and return the earliest time over all the provider/accounts. If none are
684 * failing then return 0.
685 */
686 public long getInitialSyncFailureTime() {
687 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
688 // Join the settings for a provider with the status so that we can easily
689 // check if each provider is enabled for syncing. We also join in the overall
690 // enabled flag ("listen_for_tickles") to each row so that we don't need to
691 // make a separate DB lookup to access it.
692 Cursor c = db.rawQuery(""
693 + "SELECT initialFailureTime, s1.value, s2.value "
694 + "FROM status "
695 + "LEFT JOIN stats ON status.stats_id=stats._id "
696 + "LEFT JOIN settings as s1 ON 'sync_provider_' || authority=s1.name "
697 + "LEFT JOIN settings as s2 ON s2.name='listen_for_tickles' "
698 + "where initialFailureTime is not null "
699 + " AND lastFailureMesg!=" + Sync.History.ERROR_TOO_MANY_DELETIONS
700 + " AND lastFailureMesg!=" + Sync.History.ERROR_AUTHENTICATION
701 + " AND lastFailureMesg!=" + Sync.History.ERROR_SYNC_ALREADY_IN_PROGRESS
702 + " AND authority!='subscribedfeeds' "
703 + " ORDER BY initialFailureTime", null);
704 try {
705 while (c.moveToNext()) {
706 // these settings default to true, so if they are null treat them as enabled
707 final String providerEnabledString = c.getString(1);
708 if (providerEnabledString != null && !Boolean.parseBoolean(providerEnabledString)) {
709 continue;
710 }
711 final String allEnabledString = c.getString(2);
712 if (allEnabledString != null && !Boolean.parseBoolean(allEnabledString)) {
713 continue;
714 }
715 return c.getLong(0);
716 }
717 } finally {
718 c.close();
719 }
720 return 0;
721 }
722
723 private void createStatusRowIfNecessary(long statsId) {
724 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
725 boolean statusExists = 0 != DatabaseUtils.longForQuery(db,
726 "SELECT count(*) FROM status WHERE stats_id=" + statsId, null);
727 if (!statusExists) {
728 ContentValues values = new ContentValues();
729 values.put("stats_id", statsId);
730 db.insert("status", null, values);
731 }
732 }
733
734 private long createStatsRowIfNecessary(String account, String authority) {
735 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
736 StringBuilder where = new StringBuilder();
737 where.append(Sync.Stats.ACCOUNT + "= ?");
738 where.append(" and " + Sync.Stats.AUTHORITY + "= ?");
739 Cursor cursor = query(Sync.Stats.CONTENT_URI,
740 Sync.Stats.SYNC_STATS_PROJECTION,
741 where.toString(), new String[] { account, authority },
742 null /* order */);
743 try {
744 long id;
745 if (cursor.moveToFirst()) {
746 id = cursor.getLong(cursor.getColumnIndexOrThrow(Sync.Stats._ID));
747 } else {
748 ContentValues values = new ContentValues();
749 values.put(Sync.Stats.ACCOUNT, account);
750 values.put(Sync.Stats.AUTHORITY, authority);
751 id = db.insert("stats", null, values);
752 }
753 return id;
754 } finally {
755 cursor.close();
756 }
757 }
758}