diff options
Diffstat (limited to 'core/java/android/content/AbstractSyncableContentProvider.java')
-rw-r--r-- | core/java/android/content/AbstractSyncableContentProvider.java | 601 |
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); + } +} |