FileDocCategorySizeDatePackage
AbstractSyncableContentProvider.javaAPI DocAndroid 1.5 API22397Wed May 06 22:41:54 BST 2009android.content

AbstractSyncableContentProvider

public abstract class AbstractSyncableContentProvider extends SyncableContentProvider
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

Fields Summary
private static final String
TAG
protected android.database.sqlite.SQLiteOpenHelper
mOpenHelper
protected android.database.sqlite.SQLiteDatabase
mDb
private final String
mDatabaseName
private final int
mDatabaseVersion
private final android.net.Uri
mContentUri
private android.accounts.AccountMonitor
mAccountMonitor
private String
mSyncingAccount
the account set in the last call to onSyncStart()
private SyncStateContentProviderHelper
mSyncState
private static final String[]
sAccountProjection
private boolean
mIsTemporary
private AbstractTableMerger
mCurrentMerger
private boolean
mIsMergeCancelled
private static final String
SYNC_ACCOUNT_WHERE_CLAUSE
private boolean
mContainsDiffs
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.
Constructors Summary
public AbstractSyncableContentProvider(String dbName, int dbVersion, android.net.Uri contentUri)
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

        super();

        mDatabaseName = dbName;
        mDatabaseVersion = dbVersion;
        mContentUri = contentUri;
        mIsTemporary = false;
        setContainsDiffs(false);
        if (Config.LOGV) {
            Log.v(TAG, "created SyncableContentProvider " + this);
        }
    
Methods Summary
protected voidbootstrapDatabase(android.database.sqlite.SQLiteDatabase db)
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.

public final intbulkInsert(android.net.Uri uri, 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;
    
public booleanchangeRequiresLocalSync(android.net.Uri uri)
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

        return true;
    
public voidclose()
Close resources that must be closed. You must call this to properly release the resources used by the AbstractSyncableContentProvider.

        if (mOpenHelper != null) {
            mOpenHelper.close();  // OK to call .close() repeatedly.
        }
    
public final intdelete(android.net.Uri url, java.lang.String selection, java.lang.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();
        }
    
protected abstract intdeleteInternal(android.net.Uri url, java.lang.String selection, java.lang.String[] selectionArgs)
Subclasses should override this instead of delete(). See delete() for details.

This method is called within a acquireDbLock()/releaseDbLock() block, which means a database transaction will be active during the call;

protected voiddeleteRowsForRemovedAccounts(java.util.Map accounts, java.lang.String table, java.lang.String accountColumnName)
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.

        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();
        }
    
public booleangetContainsDiffs()

        return mContainsDiffs;
    
public android.database.sqlite.SQLiteDatabasegetDatabase()

       if (mDb == null) mDb = mOpenHelper.getWritableDatabase();
       return mDb;
    
protected java.lang.IterablegetMergers()
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.

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.

        return Collections.emptyList();
    
public java.lang.StringgetSyncingAccount()
The account of the most recent call to onSyncStart()

return
the account

        return mSyncingAccount;
    
public android.content.AbstractSyncableContentProvidergetTemporaryInstance()
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.

        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 final android.net.Uriinsert(android.net.Uri url, 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();
        }
    
protected abstract android.net.UriinsertInternal(android.net.Uri url, ContentValues values)
Subclasses should override this instead of insert(). See insert() for details.

This method is called within a acquireDbLock()/releaseDbLock() block, which means a database transaction will be active during the call;

public booleanisMergeCancelled()

        return mIsMergeCancelled;
    
protected booleanisTemporary()


       
        return mIsTemporary;
    
public voidmerge(SyncContext context, SyncableContentProvider diffs, TempProviderSyncResult result, SyncResult syncResult)
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

        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();
        }
    
protected voidonAccountsChanged(java.lang.String[] accountsArray)
Make sure that there are no entries for accounts that no longer exist

param
accountsArray the array of currently-existing accounts

        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();
        }
    
public booleanonCreate()

        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;
    
protected voidonDatabaseOpened(android.database.sqlite.SQLiteDatabase db)
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.

public voidonSyncCanceled()
Invoked when the active sync has been canceled. Sets the sync state of this provider and its merger to canceled.

        synchronized (this) {
            mIsMergeCancelled = true;
            if (mCurrentMerger != null) {
                mCurrentMerger.onMergeCancelled();
            }
        }
    
public voidonSyncStart(SyncContext context, java.lang.String account)
Called right before a sync is started.

param
context the sync context for the operation
param
account

        if (TextUtils.isEmpty(account)) {
            throw new IllegalArgumentException("you passed in an empty account");
        }
        mSyncingAccount = account;
    
public voidonSyncStop(SyncContext context, boolean success)
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 final android.database.Cursorquery(android.net.Uri url, java.lang.String[] projection, java.lang.String selection, java.lang.String[] selectionArgs, java.lang.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);
    
protected abstract android.database.CursorqueryInternal(android.net.Uri url, java.lang.String[] projection, java.lang.String selection, java.lang.String[] selectionArgs, java.lang.String sortOrder)
Subclasses should override this instead of query(). See query() for details.

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.

public byte[]readSyncDataBytes(java.lang.String account)
Retrieves the SyncData bytes for the given account. The byte array returned may be null.

        return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account);
    
public voidsetContainsDiffs(boolean containsDiffs)

        if (containsDiffs && !isTemporary()) {
            throw new IllegalStateException(
                    "only a temporary provider can contain diffs");
        }
        mContainsDiffs = containsDiffs;
    
public final intupdate(android.net.Uri url, ContentValues values, java.lang.String selection, java.lang.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();
        }
    
protected abstract intupdateInternal(android.net.Uri url, ContentValues values, java.lang.String selection, java.lang.String[] selectionArgs)
Subclasses should override this instead of update(). See update() for details.

This method is called within a acquireDbLock()/releaseDbLock() block, which means a database transaction will be active during the call;

protected abstract booleanupgradeDatabase(android.database.sqlite.SQLiteDatabase db, int oldVersion, int newVersion)
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

public voidwipeAccount(java.lang.String account)
Called when the sync system determines that this provider should no longer contain records for the specified 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();
        }
    
public voidwriteSyncDataBytes(java.lang.String account, byte[] data)
Sets the SyncData bytes for the given account. The byte array may be null.

        mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data);