blob: ce6501c9f147602b27f2b8d278108415204507fd [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001package android.content;
2
3import android.database.sqlite.SQLiteOpenHelper;
4import android.database.sqlite.SQLiteDatabase;
5import android.database.Cursor;
6import android.net.Uri;
7import android.accounts.AccountMonitor;
8import android.accounts.AccountMonitorListener;
9import android.provider.SyncConstValue;
10import android.util.Config;
11import android.util.Log;
12import android.os.Bundle;
13import android.text.TextUtils;
14
15import java.util.Collections;
16import java.util.Map;
17import java.util.HashMap;
18import java.util.Vector;
19import java.util.ArrayList;
20
21/**
22 * A specialization of the ContentProvider that centralizes functionality
23 * used by ContentProviders that are syncable. It also wraps calls to the ContentProvider
24 * inside of database transactions.
25 *
26 * @hide
27 */
28public abstract class AbstractSyncableContentProvider extends SyncableContentProvider {
29 private static final String TAG = "SyncableContentProvider";
30 protected SQLiteOpenHelper mOpenHelper;
31 protected SQLiteDatabase mDb;
32 private final String mDatabaseName;
33 private final int mDatabaseVersion;
34 private final Uri mContentUri;
35 private AccountMonitor mAccountMonitor;
36
37 /** the account set in the last call to onSyncStart() */
38 private String mSyncingAccount;
39
40 private SyncStateContentProviderHelper mSyncState = null;
41
42 private static final String[] sAccountProjection = new String[] {SyncConstValue._SYNC_ACCOUNT};
43
44 private boolean mIsTemporary;
45
46 private AbstractTableMerger mCurrentMerger = null;
47 private boolean mIsMergeCancelled = false;
48
49 private static final String SYNC_ACCOUNT_WHERE_CLAUSE = SyncConstValue._SYNC_ACCOUNT + "=?";
50
51 protected boolean isTemporary() {
52 return mIsTemporary;
53 }
54
55 /**
56 * Indicates whether or not this ContentProvider contains a full
57 * set of data or just diffs. This knowledge comes in handy when
58 * determining how to incorporate the contents of a temporary
59 * provider into a real provider.
60 */
61 private boolean mContainsDiffs;
62
63 /**
64 * Initializes the AbstractSyncableContentProvider
65 * @param dbName the filename of the database
66 * @param dbVersion the current version of the database schema
67 * @param contentUri The base Uri of the syncable content in this provider
68 */
69 public AbstractSyncableContentProvider(String dbName, int dbVersion, Uri contentUri) {
70 super();
71
72 mDatabaseName = dbName;
73 mDatabaseVersion = dbVersion;
74 mContentUri = contentUri;
75 mIsTemporary = false;
76 setContainsDiffs(false);
77 if (Config.LOGV) {
78 Log.v(TAG, "created SyncableContentProvider " + this);
79 }
80 }
81
82 /**
83 * Close resources that must be closed. You must call this to properly release
84 * the resources used by the AbstractSyncableContentProvider.
85 */
86 public void close() {
87 if (mOpenHelper != null) {
88 mOpenHelper.close(); // OK to call .close() repeatedly.
89 }
90 }
91
92 /**
93 * Override to create your schema and do anything else you need to do with a new database.
94 * This is run inside a transaction (so you don't need to use one).
95 * This method may not use getDatabase(), or call content provider methods, it must only
96 * use the database handle passed to it.
97 */
98 protected void bootstrapDatabase(SQLiteDatabase db) {}
99
100 /**
101 * Override to upgrade your database from an old version to the version you specified.
102 * Don't set the DB version; this will automatically be done after the method returns.
103 * This method may not use getDatabase(), or call content provider methods, it must only
104 * use the database handle passed to it.
105 *
106 * @param oldVersion version of the existing database
107 * @param newVersion current version to upgrade to
108 * @return true if the upgrade was lossless, false if it was lossy
109 */
110 protected abstract boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion);
111
112 /**
113 * Override to do anything (like cleanups or checks) you need to do after opening a database.
114 * Does nothing by default. This is run inside a transaction (so you don't need to use one).
115 * This method may not use getDatabase(), or call content provider methods, it must only
116 * use the database handle passed to it.
117 */
118 protected void onDatabaseOpened(SQLiteDatabase db) {}
119
120 private class DatabaseHelper extends SQLiteOpenHelper {
121 DatabaseHelper(Context context, String name) {
122 // Note: context and name may be null for temp providers
123 super(context, name, null, mDatabaseVersion);
124 }
125
126 @Override
127 public void onCreate(SQLiteDatabase db) {
128 bootstrapDatabase(db);
129 mSyncState.createDatabase(db);
130 }
131
132 @Override
133 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
134 if (!upgradeDatabase(db, oldVersion, newVersion)) {
135 mSyncState.discardSyncData(db, null /* all accounts */);
136 getContext().getContentResolver().startSync(mContentUri, new Bundle());
137 }
138 }
139
140 @Override
141 public void onOpen(SQLiteDatabase db) {
142 onDatabaseOpened(db);
143 mSyncState.onDatabaseOpened(db);
144 }
145 }
146
147 @Override
148 public boolean onCreate() {
149 if (isTemporary()) throw new IllegalStateException("onCreate() called for temp provider");
150 mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(), mDatabaseName);
151 mSyncState = new SyncStateContentProviderHelper(mOpenHelper);
152
153 AccountMonitorListener listener = new AccountMonitorListener() {
154 public void onAccountsUpdated(String[] accounts) {
155 // Some providers override onAccountsChanged(); give them a database to work with.
156 mDb = mOpenHelper.getWritableDatabase();
157 onAccountsChanged(accounts);
158 TempProviderSyncAdapter syncAdapter = (TempProviderSyncAdapter)getSyncAdapter();
159 if (syncAdapter != null) {
160 syncAdapter.onAccountsChanged(accounts);
161 }
162 }
163 };
164 mAccountMonitor = new AccountMonitor(getContext(), listener);
165
166 return true;
167 }
168
169 /**
170 * Get a non-persistent instance of this content provider.
171 * You must call {@link #close} on the returned
172 * SyncableContentProvider when you are done with it.
173 *
174 * @return a non-persistent content provider with the same layout as this
175 * provider.
176 */
177 public AbstractSyncableContentProvider getTemporaryInstance() {
178 AbstractSyncableContentProvider temp;
179 try {
180 temp = getClass().newInstance();
181 } catch (InstantiationException e) {
182 throw new RuntimeException("unable to instantiate class, "
183 + "this should never happen", e);
184 } catch (IllegalAccessException e) {
185 throw new RuntimeException(
186 "IllegalAccess while instantiating class, "
187 + "this should never happen", e);
188 }
189
190 // Note: onCreate() isn't run for the temp provider, and it has no Context.
191 temp.mIsTemporary = true;
192 temp.setContainsDiffs(true);
193 temp.mOpenHelper = temp.new DatabaseHelper(null, null);
194 temp.mSyncState = new SyncStateContentProviderHelper(temp.mOpenHelper);
195 if (!isTemporary()) {
196 mSyncState.copySyncState(
197 mOpenHelper.getReadableDatabase(),
198 temp.mOpenHelper.getWritableDatabase(),
199 getSyncingAccount());
200 }
201 return temp;
202 }
203
204 public SQLiteDatabase getDatabase() {
205 if (mDb == null) mDb = mOpenHelper.getWritableDatabase();
206 return mDb;
207 }
208
209 public boolean getContainsDiffs() {
210 return mContainsDiffs;
211 }
212
213 public void setContainsDiffs(boolean containsDiffs) {
214 if (containsDiffs && !isTemporary()) {
215 throw new IllegalStateException(
216 "only a temporary provider can contain diffs");
217 }
218 mContainsDiffs = containsDiffs;
219 }
220
221 /**
222 * Each subclass of this class should define a subclass of {@link
223 * android.content.AbstractTableMerger} for each table they wish to merge. It
224 * should then override this method and return one instance of
225 * each merger, in sequence. Their {@link
226 * android.content.AbstractTableMerger#merge merge} methods will be called, one at a
227 * time, in the order supplied.
228 *
229 * <p>The default implementation returns an empty list, so that no
230 * merging will occur.
231 * @return A sequence of subclasses of {@link
232 * android.content.AbstractTableMerger}, one for each table that should be merged.
233 */
234 protected Iterable<? extends AbstractTableMerger> getMergers() {
235 return Collections.emptyList();
236 }
237
238 @Override
239 public final int update(final Uri url, final ContentValues values,
240 final String selection, final String[] selectionArgs) {
241 mDb = mOpenHelper.getWritableDatabase();
242 mDb.beginTransaction();
243 try {
244 if (isTemporary() && mSyncState.matches(url)) {
245 int numRows = mSyncState.asContentProvider().update(
246 url, values, selection, selectionArgs);
247 mDb.setTransactionSuccessful();
248 return numRows;
249 }
250
251 int result = updateInternal(url, values, selection, selectionArgs);
252 mDb.setTransactionSuccessful();
253
254 if (!isTemporary() && result > 0) {
255 getContext().getContentResolver().notifyChange(url, null /* observer */,
256 changeRequiresLocalSync(url));
257 }
258
259 return result;
260 } finally {
261 mDb.endTransaction();
262 }
263 }
264
265 @Override
266 public final int delete(final Uri url, final String selection,
267 final String[] selectionArgs) {
268 mDb = mOpenHelper.getWritableDatabase();
269 mDb.beginTransaction();
270 try {
271 if (isTemporary() && mSyncState.matches(url)) {
272 int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs);
273 mDb.setTransactionSuccessful();
274 return numRows;
275 }
276 int result = deleteInternal(url, selection, selectionArgs);
277 mDb.setTransactionSuccessful();
278 if (!isTemporary() && result > 0) {
279 getContext().getContentResolver().notifyChange(url, null /* observer */,
280 changeRequiresLocalSync(url));
281 }
282 return result;
283 } finally {
284 mDb.endTransaction();
285 }
286 }
287
288 @Override
289 public final Uri insert(final Uri url, final ContentValues values) {
290 mDb = mOpenHelper.getWritableDatabase();
291 mDb.beginTransaction();
292 try {
293 if (isTemporary() && mSyncState.matches(url)) {
294 Uri result = mSyncState.asContentProvider().insert(url, values);
295 mDb.setTransactionSuccessful();
296 return result;
297 }
298 Uri result = insertInternal(url, values);
299 mDb.setTransactionSuccessful();
300 if (!isTemporary() && result != null) {
301 getContext().getContentResolver().notifyChange(url, null /* observer */,
302 changeRequiresLocalSync(url));
303 }
304 return result;
305 } finally {
306 mDb.endTransaction();
307 }
308 }
309
310 @Override
311 public final int bulkInsert(final Uri uri, final ContentValues[] values) {
312 int size = values.length;
313 int completed = 0;
314 final boolean isSyncStateUri = mSyncState.matches(uri);
315 mDb = mOpenHelper.getWritableDatabase();
316 mDb.beginTransaction();
317 try {
318 for (int i = 0; i < size; i++) {
319 Uri result;
320 if (isTemporary() && isSyncStateUri) {
321 result = mSyncState.asContentProvider().insert(uri, values[i]);
322 } else {
323 result = insertInternal(uri, values[i]);
324 mDb.yieldIfContended();
325 }
326 if (result != null) {
327 completed++;
328 }
329 }
330 mDb.setTransactionSuccessful();
331 } finally {
332 mDb.endTransaction();
333 }
334 if (!isTemporary() && completed == size) {
335 getContext().getContentResolver().notifyChange(uri, null /* observer */,
336 changeRequiresLocalSync(uri));
337 }
338 return completed;
339 }
340
341 /**
342 * Check if changes to this URI can be syncable changes.
343 * @param uri the URI of the resource that was changed
344 * @return true if changes to this URI can be syncable changes, false otherwise
345 */
346 public boolean changeRequiresLocalSync(Uri uri) {
347 return true;
348 }
349
350 @Override
351 public final Cursor query(final Uri url, final String[] projection,
352 final String selection, final String[] selectionArgs,
353 final String sortOrder) {
354 mDb = mOpenHelper.getReadableDatabase();
355 if (isTemporary() && mSyncState.matches(url)) {
356 return mSyncState.asContentProvider().query(
357 url, projection, selection, selectionArgs, sortOrder);
358 }
359 return queryInternal(url, projection, selection, selectionArgs, sortOrder);
360 }
361
362 /**
363 * Called right before a sync is started.
364 *
365 * @param context the sync context for the operation
366 * @param account
367 */
368 public void onSyncStart(SyncContext context, String account) {
369 if (TextUtils.isEmpty(account)) {
370 throw new IllegalArgumentException("you passed in an empty account");
371 }
372 mSyncingAccount = account;
373 }
374
375 /**
376 * Called right after a sync is completed
377 *
378 * @param context the sync context for the operation
379 * @param success true if the sync succeeded, false if an error occurred
380 */
381 public void onSyncStop(SyncContext context, boolean success) {
382 }
383
384 /**
385 * The account of the most recent call to onSyncStart()
386 * @return the account
387 */
388 public String getSyncingAccount() {
389 return mSyncingAccount;
390 }
391
392 /**
393 * Merge diffs from a sync source with this content provider.
394 *
395 * @param context the SyncContext within which this merge is taking place
396 * @param diffs A temporary content provider containing diffs from a sync
397 * source.
398 * @param result a MergeResult that contains information about the merge, including
399 * a temporary content provider with the same layout as this provider containing
400 * @param syncResult
401 */
402 public void merge(SyncContext context, SyncableContentProvider diffs,
403 TempProviderSyncResult result, SyncResult syncResult) {
404 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
405 db.beginTransaction();
406 try {
407 synchronized(this) {
408 mIsMergeCancelled = false;
409 }
410 Iterable<? extends AbstractTableMerger> mergers = getMergers();
411 try {
412 for (AbstractTableMerger merger : mergers) {
413 synchronized(this) {
414 if (mIsMergeCancelled) break;
415 mCurrentMerger = merger;
416 }
417 merger.merge(context, getSyncingAccount(), diffs, result, syncResult, this);
418 }
419 if (mIsMergeCancelled) return;
420 if (diffs != null) {
421 mSyncState.copySyncState(
422 ((AbstractSyncableContentProvider)diffs).mOpenHelper.getReadableDatabase(),
423 mOpenHelper.getWritableDatabase(),
424 getSyncingAccount());
425 }
426 } finally {
427 synchronized (this) {
428 mCurrentMerger = null;
429 }
430 }
431 db.setTransactionSuccessful();
432 } finally {
433 db.endTransaction();
434 }
435 }
436
437
438 /**
439 * Invoked when the active sync has been canceled. Sets the sync state of this provider and
440 * its merger to canceled.
441 */
442 public void onSyncCanceled() {
443 synchronized (this) {
444 mIsMergeCancelled = true;
445 if (mCurrentMerger != null) {
446 mCurrentMerger.onMergeCancelled();
447 }
448 }
449 }
450
451
452 public boolean isMergeCancelled() {
453 return mIsMergeCancelled;
454 }
455
456 /**
457 * Subclasses should override this instead of update(). See update()
458 * for details.
459 *
460 * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
461 * which means a database transaction will be active during the call;
462 */
463 protected abstract int updateInternal(Uri url, ContentValues values,
464 String selection, String[] selectionArgs);
465
466 /**
467 * Subclasses should override this instead of delete(). See delete()
468 * for details.
469 *
470 * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
471 * which means a database transaction will be active during the call;
472 */
473 protected abstract int deleteInternal(Uri url, String selection, String[] selectionArgs);
474
475 /**
476 * Subclasses should override this instead of insert(). See insert()
477 * for details.
478 *
479 * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
480 * which means a database transaction will be active during the call;
481 */
482 protected abstract Uri insertInternal(Uri url, ContentValues values);
483
484 /**
485 * Subclasses should override this instead of query(). See query()
486 * for details.
487 *
488 * <p> This method is *not* called within a acquireDbLock()/releaseDbLock()
489 * block for performance reasons. If an implementation needs atomic access
490 * to the database the lock can be acquired then.
491 */
492 protected abstract Cursor queryInternal(Uri url, String[] projection,
493 String selection, String[] selectionArgs, String sortOrder);
494
495 /**
496 * Make sure that there are no entries for accounts that no longer exist
497 * @param accountsArray the array of currently-existing accounts
498 */
499 protected void onAccountsChanged(String[] accountsArray) {
500 Map<String, Boolean> accounts = new HashMap<String, Boolean>();
501 for (String account : accountsArray) {
502 accounts.put(account, false);
503 }
504 accounts.put(SyncConstValue.NON_SYNCABLE_ACCOUNT, false);
505
506 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
507 Map<String, String> tableMap = db.getSyncedTables();
508 Vector<String> tables = new Vector<String>();
509 tables.addAll(tableMap.keySet());
510 tables.addAll(tableMap.values());
511
512 db.beginTransaction();
513 try {
514 mSyncState.onAccountsChanged(accountsArray);
515 for (String table : tables) {
516 deleteRowsForRemovedAccounts(accounts, table,
517 SyncConstValue._SYNC_ACCOUNT);
518 }
519 db.setTransactionSuccessful();
520 } finally {
521 db.endTransaction();
522 }
523 }
524
525 /**
526 * A helper method to delete all rows whose account is not in the accounts
527 * map. The accountColumnName is the name of the column that is expected
528 * to hold the account. If a row has an empty account it is never deleted.
529 *
530 * @param accounts a map of existing accounts
531 * @param table the table to delete from
532 * @param accountColumnName the name of the column that is expected
533 * to hold the account.
534 */
535 protected void deleteRowsForRemovedAccounts(Map<String, Boolean> accounts,
536 String table, String accountColumnName) {
537 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
538 Cursor c = db.query(table, sAccountProjection, null, null,
539 accountColumnName, null, null);
540 try {
541 while (c.moveToNext()) {
542 String account = c.getString(0);
543 if (TextUtils.isEmpty(account)) {
544 continue;
545 }
546 if (!accounts.containsKey(account)) {
547 int numDeleted;
548 numDeleted = db.delete(table, accountColumnName + "=?", new String[]{account});
549 if (Config.LOGV) {
550 Log.v(TAG, "deleted " + numDeleted
551 + " records from table " + table
552 + " for account " + account);
553 }
554 }
555 }
556 } finally {
557 c.close();
558 }
559 }
560
561 /**
562 * Called when the sync system determines that this provider should no longer
563 * contain records for the specified account.
564 */
565 public void wipeAccount(String account) {
566 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
567 Map<String, String> tableMap = db.getSyncedTables();
568 ArrayList<String> tables = new ArrayList<String>();
569 tables.addAll(tableMap.keySet());
570 tables.addAll(tableMap.values());
571
572 db.beginTransaction();
573
574 try {
575 // remove the SyncState data
576 mSyncState.discardSyncData(db, account);
577
578 // remove the data in the synced tables
579 for (String table : tables) {
580 db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, new String[]{account});
581 }
582 db.setTransactionSuccessful();
583 } finally {
584 db.endTransaction();
585 }
586 }
587
588 /**
589 * Retrieves the SyncData bytes for the given account. The byte array returned may be null.
590 */
591 public byte[] readSyncDataBytes(String account) {
592 return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account);
593 }
594
595 /**
596 * Sets the SyncData bytes for the given account. The byte array may be null.
597 */
598 public void writeSyncDataBytes(String account, byte[] data) {
599 mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data);
600 }
601}