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