blob: 249d9babd8a7470b10e693151802c328369bdf77 [file] [log] [blame]
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();
}
/**
* <p>
* Call mOpenHelper.getWritableDatabase() and mDb.beginTransaction().
* {@link #endTransaction} MUST be called after calling this method.
* Those methods should be used like this:
* </p>
*
* <pre class="prettyprint">
* boolean successful = false;
* beginTransaction();
* try {
* // Do something related to mDb
* successful = true;
* return ret;
* } finally {
* endTransaction(successful);
* }
* </pre>
*
* @hide This method is dangerous from the view of database manipulation, though using
* this makes batch insertion/update/delete much faster.
*/
public final void beginTransaction() {
mDb = mOpenHelper.getWritableDatabase();
mDb.beginTransaction();
}
/**
* <p>
* Call mDb.endTransaction(). If successful is true, try to call
* mDb.setTransactionSuccessful() before calling mDb.endTransaction().
* This method MUST be used with {@link #beginTransaction()}.
* </p>
*
* @hide This method is dangerous from the view of database manipulation, though using
* this makes batch insertion/update/delete much faster.
*/
public final void endTransaction(boolean successful) {
try {
if (successful) {
// setTransactionSuccessful() must be called just once during opening the
// transaction.
mDb.setTransactionSuccessful();
}
} finally {
mDb.endTransaction();
}
}
@Override
public final int update(final Uri uri, final ContentValues values,
final String selection, final String[] selectionArgs) {
boolean successful = false;
beginTransaction();
try {
int ret = nonTransactionalUpdate(uri, values, selection, selectionArgs);
successful = true;
return ret;
} finally {
endTransaction(successful);
}
}
/**
* @hide
*/
public final int nonTransactionalUpdate(final Uri uri, final ContentValues values,
final String selection, final String[] selectionArgs) {
if (isTemporary() && mSyncState.matches(uri)) {
int numRows = mSyncState.asContentProvider().update(
uri, values, selection, selectionArgs);
return numRows;
}
int result = updateInternal(uri, values, selection, selectionArgs);
if (!isTemporary() && result > 0) {
getContext().getContentResolver().notifyChange(uri, null /* observer */,
changeRequiresLocalSync(uri));
}
return result;
}
@Override
public final int delete(final Uri uri, final String selection,
final String[] selectionArgs) {
boolean successful = false;
beginTransaction();
try {
int ret = nonTransactionalDelete(uri, selection, selectionArgs);
successful = true;
return ret;
} finally {
endTransaction(successful);
}
}
/**
* @hide
*/
public final int nonTransactionalDelete(final Uri uri, final String selection,
final String[] selectionArgs) {
if (isTemporary() && mSyncState.matches(uri)) {
int numRows = mSyncState.asContentProvider().delete(uri, selection, selectionArgs);
return numRows;
}
int result = deleteInternal(uri, selection, selectionArgs);
if (!isTemporary() && result > 0) {
getContext().getContentResolver().notifyChange(uri, null /* observer */,
changeRequiresLocalSync(uri));
}
return result;
}
@Override
public final Uri insert(final Uri uri, final ContentValues values) {
boolean successful = false;
beginTransaction();
try {
Uri ret = nonTransactionalInsert(uri, values);
successful = true;
return ret;
} finally {
endTransaction(successful);
}
}
/**
* @hide
*/
public final Uri nonTransactionalInsert(final Uri uri, final ContentValues values) {
if (isTemporary() && mSyncState.matches(uri)) {
Uri result = mSyncState.asContentProvider().insert(uri, values);
return result;
}
Uri result = insertInternal(uri, values);
if (!isTemporary() && result != null) {
getContext().getContentResolver().notifyChange(uri, null /* observer */,
changeRequiresLocalSync(uri));
}
return result;
}
@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);
}
}