summaryrefslogtreecommitdiffstats
path: root/core/java/android/content/AbstractSyncableContentProvider.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/content/AbstractSyncableContentProvider.java')
-rw-r--r--core/java/android/content/AbstractSyncableContentProvider.java601
1 files changed, 601 insertions, 0 deletions
diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java
new file mode 100644
index 0000000..ce6501c
--- /dev/null
+++ b/core/java/android/content/AbstractSyncableContentProvider.java
@@ -0,0 +1,601 @@
+package android.content;
+
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.Cursor;
+import android.net.Uri;
+import android.accounts.AccountMonitor;
+import android.accounts.AccountMonitorListener;
+import android.provider.SyncConstValue;
+import android.util.Config;
+import android.util.Log;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Vector;
+import java.util.ArrayList;
+
+/**
+ * A specialization of the ContentProvider that centralizes functionality
+ * used by ContentProviders that are syncable. It also wraps calls to the ContentProvider
+ * inside of database transactions.
+ *
+ * @hide
+ */
+public abstract class AbstractSyncableContentProvider extends SyncableContentProvider {
+ private static final String TAG = "SyncableContentProvider";
+ protected SQLiteOpenHelper mOpenHelper;
+ protected SQLiteDatabase mDb;
+ private final String mDatabaseName;
+ private final int mDatabaseVersion;
+ private final Uri mContentUri;
+ private AccountMonitor mAccountMonitor;
+
+ /** the account set in the last call to onSyncStart() */
+ private String mSyncingAccount;
+
+ private SyncStateContentProviderHelper mSyncState = null;
+
+ private static final String[] sAccountProjection = new String[] {SyncConstValue._SYNC_ACCOUNT};
+
+ private boolean mIsTemporary;
+
+ private AbstractTableMerger mCurrentMerger = null;
+ private boolean mIsMergeCancelled = false;
+
+ private static final String SYNC_ACCOUNT_WHERE_CLAUSE = SyncConstValue._SYNC_ACCOUNT + "=?";
+
+ protected boolean isTemporary() {
+ return mIsTemporary;
+ }
+
+ /**
+ * Indicates whether or not this ContentProvider contains a full
+ * set of data or just diffs. This knowledge comes in handy when
+ * determining how to incorporate the contents of a temporary
+ * provider into a real provider.
+ */
+ private boolean mContainsDiffs;
+
+ /**
+ * Initializes the AbstractSyncableContentProvider
+ * @param dbName the filename of the database
+ * @param dbVersion the current version of the database schema
+ * @param contentUri The base Uri of the syncable content in this provider
+ */
+ public AbstractSyncableContentProvider(String dbName, int dbVersion, Uri contentUri) {
+ super();
+
+ mDatabaseName = dbName;
+ mDatabaseVersion = dbVersion;
+ mContentUri = contentUri;
+ mIsTemporary = false;
+ setContainsDiffs(false);
+ if (Config.LOGV) {
+ Log.v(TAG, "created SyncableContentProvider " + this);
+ }
+ }
+
+ /**
+ * Close resources that must be closed. You must call this to properly release
+ * the resources used by the AbstractSyncableContentProvider.
+ */
+ public void close() {
+ if (mOpenHelper != null) {
+ mOpenHelper.close(); // OK to call .close() repeatedly.
+ }
+ }
+
+ /**
+ * Override to create your schema and do anything else you need to do with a new database.
+ * This is run inside a transaction (so you don't need to use one).
+ * This method may not use getDatabase(), or call content provider methods, it must only
+ * use the database handle passed to it.
+ */
+ protected void bootstrapDatabase(SQLiteDatabase db) {}
+
+ /**
+ * Override to upgrade your database from an old version to the version you specified.
+ * Don't set the DB version; this will automatically be done after the method returns.
+ * This method may not use getDatabase(), or call content provider methods, it must only
+ * use the database handle passed to it.
+ *
+ * @param oldVersion version of the existing database
+ * @param newVersion current version to upgrade to
+ * @return true if the upgrade was lossless, false if it was lossy
+ */
+ protected abstract boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion);
+
+ /**
+ * Override to do anything (like cleanups or checks) you need to do after opening a database.
+ * Does nothing by default. This is run inside a transaction (so you don't need to use one).
+ * This method may not use getDatabase(), or call content provider methods, it must only
+ * use the database handle passed to it.
+ */
+ protected void onDatabaseOpened(SQLiteDatabase db) {}
+
+ private class DatabaseHelper extends SQLiteOpenHelper {
+ DatabaseHelper(Context context, String name) {
+ // Note: context and name may be null for temp providers
+ super(context, name, null, mDatabaseVersion);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ bootstrapDatabase(db);
+ mSyncState.createDatabase(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (!upgradeDatabase(db, oldVersion, newVersion)) {
+ mSyncState.discardSyncData(db, null /* all accounts */);
+ getContext().getContentResolver().startSync(mContentUri, new Bundle());
+ }
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ onDatabaseOpened(db);
+ mSyncState.onDatabaseOpened(db);
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ if (isTemporary()) throw new IllegalStateException("onCreate() called for temp provider");
+ mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(), mDatabaseName);
+ mSyncState = new SyncStateContentProviderHelper(mOpenHelper);
+
+ AccountMonitorListener listener = new AccountMonitorListener() {
+ public void onAccountsUpdated(String[] accounts) {
+ // Some providers override onAccountsChanged(); give them a database to work with.
+ mDb = mOpenHelper.getWritableDatabase();
+ onAccountsChanged(accounts);
+ TempProviderSyncAdapter syncAdapter = (TempProviderSyncAdapter)getSyncAdapter();
+ if (syncAdapter != null) {
+ syncAdapter.onAccountsChanged(accounts);
+ }
+ }
+ };
+ mAccountMonitor = new AccountMonitor(getContext(), listener);
+
+ return true;
+ }
+
+ /**
+ * Get a non-persistent instance of this content provider.
+ * You must call {@link #close} on the returned
+ * SyncableContentProvider when you are done with it.
+ *
+ * @return a non-persistent content provider with the same layout as this
+ * provider.
+ */
+ public AbstractSyncableContentProvider getTemporaryInstance() {
+ AbstractSyncableContentProvider temp;
+ try {
+ temp = getClass().newInstance();
+ } catch (InstantiationException e) {
+ throw new RuntimeException("unable to instantiate class, "
+ + "this should never happen", e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(
+ "IllegalAccess while instantiating class, "
+ + "this should never happen", e);
+ }
+
+ // Note: onCreate() isn't run for the temp provider, and it has no Context.
+ temp.mIsTemporary = true;
+ temp.setContainsDiffs(true);
+ temp.mOpenHelper = temp.new DatabaseHelper(null, null);
+ temp.mSyncState = new SyncStateContentProviderHelper(temp.mOpenHelper);
+ if (!isTemporary()) {
+ mSyncState.copySyncState(
+ mOpenHelper.getReadableDatabase(),
+ temp.mOpenHelper.getWritableDatabase(),
+ getSyncingAccount());
+ }
+ return temp;
+ }
+
+ public SQLiteDatabase getDatabase() {
+ if (mDb == null) mDb = mOpenHelper.getWritableDatabase();
+ return mDb;
+ }
+
+ public boolean getContainsDiffs() {
+ return mContainsDiffs;
+ }
+
+ public void setContainsDiffs(boolean containsDiffs) {
+ if (containsDiffs && !isTemporary()) {
+ throw new IllegalStateException(
+ "only a temporary provider can contain diffs");
+ }
+ mContainsDiffs = containsDiffs;
+ }
+
+ /**
+ * Each subclass of this class should define a subclass of {@link
+ * android.content.AbstractTableMerger} for each table they wish to merge. It
+ * should then override this method and return one instance of
+ * each merger, in sequence. Their {@link
+ * android.content.AbstractTableMerger#merge merge} methods will be called, one at a
+ * time, in the order supplied.
+ *
+ * <p>The default implementation returns an empty list, so that no
+ * merging will occur.
+ * @return A sequence of subclasses of {@link
+ * android.content.AbstractTableMerger}, one for each table that should be merged.
+ */
+ protected Iterable<? extends AbstractTableMerger> getMergers() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public final int update(final Uri url, final ContentValues values,
+ final String selection, final String[] selectionArgs) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ if (isTemporary() && mSyncState.matches(url)) {
+ int numRows = mSyncState.asContentProvider().update(
+ url, values, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+ return numRows;
+ }
+
+ int result = updateInternal(url, values, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+
+ if (!isTemporary() && result > 0) {
+ getContext().getContentResolver().notifyChange(url, null /* observer */,
+ changeRequiresLocalSync(url));
+ }
+
+ return result;
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ @Override
+ public final int delete(final Uri url, final String selection,
+ final String[] selectionArgs) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ if (isTemporary() && mSyncState.matches(url)) {
+ int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+ return numRows;
+ }
+ int result = deleteInternal(url, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+ if (!isTemporary() && result > 0) {
+ getContext().getContentResolver().notifyChange(url, null /* observer */,
+ changeRequiresLocalSync(url));
+ }
+ return result;
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ @Override
+ public final Uri insert(final Uri url, final ContentValues values) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ if (isTemporary() && mSyncState.matches(url)) {
+ Uri result = mSyncState.asContentProvider().insert(url, values);
+ mDb.setTransactionSuccessful();
+ return result;
+ }
+ Uri result = insertInternal(url, values);
+ mDb.setTransactionSuccessful();
+ if (!isTemporary() && result != null) {
+ getContext().getContentResolver().notifyChange(url, null /* observer */,
+ changeRequiresLocalSync(url));
+ }
+ return result;
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ @Override
+ public final int bulkInsert(final Uri uri, final ContentValues[] values) {
+ int size = values.length;
+ int completed = 0;
+ final boolean isSyncStateUri = mSyncState.matches(uri);
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ for (int i = 0; i < size; i++) {
+ Uri result;
+ if (isTemporary() && isSyncStateUri) {
+ result = mSyncState.asContentProvider().insert(uri, values[i]);
+ } else {
+ result = insertInternal(uri, values[i]);
+ mDb.yieldIfContended();
+ }
+ if (result != null) {
+ completed++;
+ }
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+ if (!isTemporary() && completed == size) {
+ getContext().getContentResolver().notifyChange(uri, null /* observer */,
+ changeRequiresLocalSync(uri));
+ }
+ return completed;
+ }
+
+ /**
+ * Check if changes to this URI can be syncable changes.
+ * @param uri the URI of the resource that was changed
+ * @return true if changes to this URI can be syncable changes, false otherwise
+ */
+ public boolean changeRequiresLocalSync(Uri uri) {
+ return true;
+ }
+
+ @Override
+ public final Cursor query(final Uri url, final String[] projection,
+ final String selection, final String[] selectionArgs,
+ final String sortOrder) {
+ mDb = mOpenHelper.getReadableDatabase();
+ if (isTemporary() && mSyncState.matches(url)) {
+ return mSyncState.asContentProvider().query(
+ url, projection, selection, selectionArgs, sortOrder);
+ }
+ return queryInternal(url, projection, selection, selectionArgs, sortOrder);
+ }
+
+ /**
+ * Called right before a sync is started.
+ *
+ * @param context the sync context for the operation
+ * @param account
+ */
+ public void onSyncStart(SyncContext context, String account) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("you passed in an empty account");
+ }
+ mSyncingAccount = account;
+ }
+
+ /**
+ * Called right after a sync is completed
+ *
+ * @param context the sync context for the operation
+ * @param success true if the sync succeeded, false if an error occurred
+ */
+ public void onSyncStop(SyncContext context, boolean success) {
+ }
+
+ /**
+ * The account of the most recent call to onSyncStart()
+ * @return the account
+ */
+ public String getSyncingAccount() {
+ return mSyncingAccount;
+ }
+
+ /**
+ * Merge diffs from a sync source with this content provider.
+ *
+ * @param context the SyncContext within which this merge is taking place
+ * @param diffs A temporary content provider containing diffs from a sync
+ * source.
+ * @param result a MergeResult that contains information about the merge, including
+ * a temporary content provider with the same layout as this provider containing
+ * @param syncResult
+ */
+ public void merge(SyncContext context, SyncableContentProvider diffs,
+ TempProviderSyncResult result, SyncResult syncResult) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ synchronized(this) {
+ mIsMergeCancelled = false;
+ }
+ Iterable<? extends AbstractTableMerger> mergers = getMergers();
+ try {
+ for (AbstractTableMerger merger : mergers) {
+ synchronized(this) {
+ if (mIsMergeCancelled) break;
+ mCurrentMerger = merger;
+ }
+ merger.merge(context, getSyncingAccount(), diffs, result, syncResult, this);
+ }
+ if (mIsMergeCancelled) return;
+ if (diffs != null) {
+ mSyncState.copySyncState(
+ ((AbstractSyncableContentProvider)diffs).mOpenHelper.getReadableDatabase(),
+ mOpenHelper.getWritableDatabase(),
+ getSyncingAccount());
+ }
+ } finally {
+ synchronized (this) {
+ mCurrentMerger = null;
+ }
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+
+ /**
+ * Invoked when the active sync has been canceled. Sets the sync state of this provider and
+ * its merger to canceled.
+ */
+ public void onSyncCanceled() {
+ synchronized (this) {
+ mIsMergeCancelled = true;
+ if (mCurrentMerger != null) {
+ mCurrentMerger.onMergeCancelled();
+ }
+ }
+ }
+
+
+ public boolean isMergeCancelled() {
+ return mIsMergeCancelled;
+ }
+
+ /**
+ * Subclasses should override this instead of update(). See update()
+ * for details.
+ *
+ * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
+ * which means a database transaction will be active during the call;
+ */
+ protected abstract int updateInternal(Uri url, ContentValues values,
+ String selection, String[] selectionArgs);
+
+ /**
+ * Subclasses should override this instead of delete(). See delete()
+ * for details.
+ *
+ * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
+ * which means a database transaction will be active during the call;
+ */
+ protected abstract int deleteInternal(Uri url, String selection, String[] selectionArgs);
+
+ /**
+ * Subclasses should override this instead of insert(). See insert()
+ * for details.
+ *
+ * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
+ * which means a database transaction will be active during the call;
+ */
+ protected abstract Uri insertInternal(Uri url, ContentValues values);
+
+ /**
+ * Subclasses should override this instead of query(). See query()
+ * for details.
+ *
+ * <p> This method is *not* called within a acquireDbLock()/releaseDbLock()
+ * block for performance reasons. If an implementation needs atomic access
+ * to the database the lock can be acquired then.
+ */
+ protected abstract Cursor queryInternal(Uri url, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder);
+
+ /**
+ * Make sure that there are no entries for accounts that no longer exist
+ * @param accountsArray the array of currently-existing accounts
+ */
+ protected void onAccountsChanged(String[] accountsArray) {
+ Map<String, Boolean> accounts = new HashMap<String, Boolean>();
+ for (String account : accountsArray) {
+ accounts.put(account, false);
+ }
+ accounts.put(SyncConstValue.NON_SYNCABLE_ACCOUNT, false);
+
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Map<String, String> tableMap = db.getSyncedTables();
+ Vector<String> tables = new Vector<String>();
+ tables.addAll(tableMap.keySet());
+ tables.addAll(tableMap.values());
+
+ db.beginTransaction();
+ try {
+ mSyncState.onAccountsChanged(accountsArray);
+ for (String table : tables) {
+ deleteRowsForRemovedAccounts(accounts, table,
+ SyncConstValue._SYNC_ACCOUNT);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * A helper method to delete all rows whose account is not in the accounts
+ * map. The accountColumnName is the name of the column that is expected
+ * to hold the account. If a row has an empty account it is never deleted.
+ *
+ * @param accounts a map of existing accounts
+ * @param table the table to delete from
+ * @param accountColumnName the name of the column that is expected
+ * to hold the account.
+ */
+ protected void deleteRowsForRemovedAccounts(Map<String, Boolean> accounts,
+ String table, String accountColumnName) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Cursor c = db.query(table, sAccountProjection, null, null,
+ accountColumnName, null, null);
+ try {
+ while (c.moveToNext()) {
+ String account = c.getString(0);
+ if (TextUtils.isEmpty(account)) {
+ continue;
+ }
+ if (!accounts.containsKey(account)) {
+ int numDeleted;
+ numDeleted = db.delete(table, accountColumnName + "=?", new String[]{account});
+ if (Config.LOGV) {
+ Log.v(TAG, "deleted " + numDeleted
+ + " records from table " + table
+ + " for account " + account);
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Called when the sync system determines that this provider should no longer
+ * contain records for the specified account.
+ */
+ public void wipeAccount(String account) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Map<String, String> tableMap = db.getSyncedTables();
+ ArrayList<String> tables = new ArrayList<String>();
+ tables.addAll(tableMap.keySet());
+ tables.addAll(tableMap.values());
+
+ db.beginTransaction();
+
+ try {
+ // remove the SyncState data
+ mSyncState.discardSyncData(db, account);
+
+ // remove the data in the synced tables
+ for (String table : tables) {
+ db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, new String[]{account});
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Retrieves the SyncData bytes for the given account. The byte array returned may be null.
+ */
+ public byte[] readSyncDataBytes(String account) {
+ return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account);
+ }
+
+ /**
+ * Sets the SyncData bytes for the given account. The byte array may be null.
+ */
+ public void writeSyncDataBytes(String account, byte[] data) {
+ mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data);
+ }
+}