diff options
-rw-r--r-- | api/current.xml | 13 | ||||
-rw-r--r-- | core/java/android/content/AbstractSyncableContentProvider.java | 758 | ||||
-rw-r--r-- | core/java/android/content/AbstractTableMerger.java | 599 | ||||
-rw-r--r-- | core/java/android/content/AbstractThreadedSyncAdapter.java | 13 | ||||
-rw-r--r-- | core/java/android/content/SyncAdapter.java | 79 | ||||
-rw-r--r-- | core/java/android/content/SyncStateContentProviderHelper.java | 243 | ||||
-rw-r--r-- | core/java/android/content/SyncableContentProvider.java | 237 | ||||
-rw-r--r-- | core/java/android/content/TempProviderSyncAdapter.java | 585 | ||||
-rw-r--r-- | core/java/android/content/TempProviderSyncResult.java | 36 | ||||
-rw-r--r-- | core/java/android/provider/ContactsContract.java | 7 | ||||
-rw-r--r-- | preloaded-classes | 8 | ||||
-rw-r--r-- | tests/FrameworkTest/tests/src/android/content/AbstractTableMergerTest.java | 587 |
12 files changed, 31 insertions, 3134 deletions
diff --git a/api/current.xml b/api/current.xml index 346aed7..b6fb64b 100644 --- a/api/current.xml +++ b/api/current.xml @@ -28148,6 +28148,19 @@ <parameter name="syncResult" type="android.content.SyncResult"> </parameter> </method> +<method name="onSyncCanceled" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="thread" type="java.lang.Thread"> +</parameter> +</method> <field name="LOG_SYNC_DETAILS" type="int" transient="false" diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java deleted file mode 100644 index 5903c83..0000000 --- a/core/java/android/content/AbstractSyncableContentProvider.java +++ /dev/null @@ -1,758 +0,0 @@ -package android.content; - -import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteDatabase; -import android.database.Cursor; -import android.net.Uri; -import android.accounts.OnAccountsUpdateListener; -import android.accounts.Account; -import android.accounts.AccountManager; -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.Vector; -import java.util.ArrayList; -import java.util.Set; -import java.util.HashSet; - -import com.google.android.collect.Maps; - -/** - * 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; - - /** the account set in the last call to onSyncStart() */ - private Account mSyncingAccount; - - private SyncStateContentProviderHelper mSyncState = null; - - private static final String[] sAccountProjection = - new String[] {SyncConstValue._SYNC_ACCOUNT, SyncConstValue._SYNC_ACCOUNT_TYPE}; - - private boolean mIsTemporary; - - private AbstractTableMerger mCurrentMerger = null; - private boolean mIsMergeCancelled = false; - - private static final String SYNC_ACCOUNT_WHERE_CLAUSE = - SyncConstValue._SYNC_ACCOUNT + "=? AND " + SyncConstValue._SYNC_ACCOUNT_TYPE + "=?"; - - protected boolean isTemporary() { - return mIsTemporary; - } - - private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>(); - private final ThreadLocal<Set<Uri>> mPendingBatchNotifications = new ThreadLocal<Set<Uri>>(); - - /** - * 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); - if (!isTemporary()) { - ContentResolver.requestSync(null /* all accounts */, - mContentUri.getAuthority(), new Bundle()); - } - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (!upgradeDatabase(db, oldVersion, newVersion)) { - mSyncState.discardSyncData(db, null /* all accounts */); - ContentResolver.requestSync(null /* all accounts */, - mContentUri.getAuthority(), 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); - AccountManager.get(getContext()).addOnAccountsUpdatedListener( - new OnAccountsUpdateListener() { - public void onAccountsUpdated(Account[] accounts) { - // Some providers override onAccountsChanged(); give them a database to - // work with. - mDb = mOpenHelper.getWritableDatabase(); - // Only call onAccountsChanged on GAIA accounts; otherwise, the contacts and - // calendar providers will choke as they try to sync unknown accounts with - // AbstractGDataSyncAdapter, which will put acore into a crash loop - ArrayList<Account> gaiaAccounts = new ArrayList<Account>(); - for (Account acct: accounts) { - if (acct.type.equals("com.google")) { - gaiaAccounts.add(acct); - } - } - accounts = new Account[gaiaAccounts.size()]; - int i = 0; - for (Account acct: gaiaAccounts) { - accounts[i++] = acct; - } - onAccountsChanged(accounts); - TempProviderSyncAdapter syncAdapter = getTempProviderSyncAdapter(); - if (syncAdapter != null) { - syncAdapter.onAccountsChanged(accounts); - } - } - }, null /* handler */, true /* updateImmediately */); - - 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(); - final boolean notApplyingBatch = !applyingBatch(); - if (notApplyingBatch) { - mDb.beginTransaction(); - } - try { - if (isTemporary() && mSyncState.matches(url)) { - int numRows = mSyncState.asContentProvider().update( - url, values, selection, selectionArgs); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - return numRows; - } - - int result = updateInternal(url, values, selection, selectionArgs); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - if (!isTemporary() && result > 0) { - if (notApplyingBatch) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } else { - mPendingBatchNotifications.get().add(url); - } - } - return result; - } finally { - if (notApplyingBatch) { - mDb.endTransaction(); - } - } - } - - @Override - public final int delete(final Uri url, final String selection, - final String[] selectionArgs) { - mDb = mOpenHelper.getWritableDatabase(); - final boolean notApplyingBatch = !applyingBatch(); - if (notApplyingBatch) { - mDb.beginTransaction(); - } - try { - if (isTemporary() && mSyncState.matches(url)) { - int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - return numRows; - } - int result = deleteInternal(url, selection, selectionArgs); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - if (!isTemporary() && result > 0) { - if (notApplyingBatch) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } else { - mPendingBatchNotifications.get().add(url); - } - } - return result; - } finally { - if (notApplyingBatch) { - mDb.endTransaction(); - } - } - } - - private boolean applyingBatch() { - return mApplyingBatch.get() != null && mApplyingBatch.get(); - } - - @Override - public final Uri insert(final Uri url, final ContentValues values) { - mDb = mOpenHelper.getWritableDatabase(); - final boolean notApplyingBatch = !applyingBatch(); - if (notApplyingBatch) { - mDb.beginTransaction(); - } - try { - if (isTemporary() && mSyncState.matches(url)) { - Uri result = mSyncState.asContentProvider().insert(url, values); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - return result; - } - Uri result = insertInternal(url, values); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - if (!isTemporary() && result != null) { - if (notApplyingBatch) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } else { - mPendingBatchNotifications.get().add(url); - } - } - return result; - } finally { - if (notApplyingBatch) { - 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; - } - - /** - * <p> - * Start batch transaction. {@link #endTransaction} MUST be called after - * calling this method. Those methods should be used like this: - * </p> - * - * <pre class="prettyprint"> - * boolean successful = false; - * beginBatch() - * try { - * // Do something related to mDb - * successful = true; - * return ret; - * } finally { - * endBatch(successful); - * } - * </pre> - * - * @hide This method should be used only when {@link ContentProvider#applyBatch} is not enough and must be - * used with {@link #endBatch}. - * e.g. If returned value has to be used during one transaction, this method might be useful. - */ - public final void beginBatch() { - // initialize if this is the first time this thread has applied a batch - if (mApplyingBatch.get() == null) { - mApplyingBatch.set(false); - mPendingBatchNotifications.set(new HashSet<Uri>()); - } - - if (applyingBatch()) { - throw new IllegalStateException( - "applyBatch is not reentrant but mApplyingBatch is already set"); - } - SQLiteDatabase db = getDatabase(); - db.beginTransaction(); - boolean successful = false; - try { - mApplyingBatch.set(true); - successful = true; - } finally { - if (!successful) { - // Something unexpected happened. We must call endTransaction() at least. - db.endTransaction(); - } - } - } - - /** - * <p> - * Finish batch transaction. If "successful" is true, try to call - * mDb.setTransactionSuccessful() before calling mDb.endTransaction(). - * This method MUST be used with {@link #beginBatch()}. - * </p> - * - * @hide This method must be used with {@link #beginTransaction} - */ - public final void endBatch(boolean successful) { - try { - if (successful) { - // setTransactionSuccessful() must be called just once during opening the - // transaction. - mDb.setTransactionSuccessful(); - } - } finally { - mApplyingBatch.set(false); - getDatabase().endTransaction(); - for (Uri url : mPendingBatchNotifications.get()) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } - } - } - - public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) - throws OperationApplicationException { - boolean successful = false; - beginBatch(); - try { - ContentProviderResult[] results = super.applyBatch(operations); - successful = true; - return results; - } finally { - endBatch(successful); - } - } - - /** - * 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, Account account) { - if (account == null) { - 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 Account 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(Account[] accountsArray) { - Map<Account, Boolean> accounts = Maps.newHashMap(); - for (Account account : accountsArray) { - accounts.put(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); - } - 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 - */ - protected void deleteRowsForRemovedAccounts(Map<Account, Boolean> accounts, String table) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - Cursor c = db.query(table, sAccountProjection, null, null, - "_sync_account, _sync_account_type", null, null); - try { - while (c.moveToNext()) { - String accountName = c.getString(0); - String accountType = c.getString(1); - if (TextUtils.isEmpty(accountName)) { - continue; - } - Account account = new Account(accountName, accountType); - if (!accounts.containsKey(account)) { - int numDeleted; - numDeleted = db.delete(table, "_sync_account=? AND _sync_account_type=?", - new String[]{account.name, account.type}); - 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(Account 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.name, account.type}); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - /** - * Retrieves the SyncData bytes for the given account. The byte array returned may be null. - */ - public byte[] readSyncDataBytes(Account account) { - return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account); - } - - /** - * Sets the SyncData bytes for the given account. The byte array may be null. - */ - public void writeSyncDataBytes(Account account, byte[] data) { - mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data); - } - - protected ContentProvider getSyncStateProvider() { - return mSyncState.asContentProvider(); - } -} diff --git a/core/java/android/content/AbstractTableMerger.java b/core/java/android/content/AbstractTableMerger.java deleted file mode 100644 index 9545fd7..0000000 --- a/core/java/android/content/AbstractTableMerger.java +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.Debug; -import android.provider.BaseColumns; -import static android.provider.SyncConstValue.*; -import android.text.TextUtils; -import android.util.Log; -import android.accounts.Account; - -/** - * @hide - */ -public abstract class AbstractTableMerger -{ - private ContentValues mValues; - - protected SQLiteDatabase mDb; - protected String mTable; - protected Uri mTableURL; - protected String mDeletedTable; - protected Uri mDeletedTableURL; - static protected ContentValues mSyncMarkValues; - static private boolean TRACE; - - static { - mSyncMarkValues = new ContentValues(); - mSyncMarkValues.put(_SYNC_MARK, 1); - TRACE = false; - } - - private static final String TAG = "AbstractTableMerger"; - private static final String[] syncDirtyProjection = - new String[] {_SYNC_DIRTY, BaseColumns._ID, _SYNC_ID, _SYNC_VERSION}; - private static final String[] syncIdAndVersionProjection = - new String[] {_SYNC_ID, _SYNC_VERSION}; - - private volatile boolean mIsMergeCancelled; - - private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " - + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?"; - - private static final String SELECT_BY_SYNC_ID_AND_ACCOUNT = - _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?"; - private static final String SELECT_BY_ID = BaseColumns._ID +"=?"; - - private static final String SELECT_UNSYNCED = - "(" + _SYNC_ACCOUNT + " IS NULL OR (" - + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?)) and " - + "(" + _SYNC_ID + " IS NULL OR (" + _SYNC_DIRTY + " > 0 and " - + _SYNC_VERSION + " IS NOT NULL))"; - - public AbstractTableMerger(SQLiteDatabase database, - String table, Uri tableURL, String deletedTable, - Uri deletedTableURL) - { - mDb = database; - mTable = table; - mTableURL = tableURL; - mDeletedTable = deletedTable; - mDeletedTableURL = deletedTableURL; - mValues = new ContentValues(); - } - - public abstract void insertRow(ContentProvider diffs, - Cursor diffsCursor); - public abstract void updateRow(long localPersonID, - ContentProvider diffs, Cursor diffsCursor); - public abstract void resolveRow(long localPersonID, - String syncID, ContentProvider diffs, Cursor diffsCursor); - - /** - * This is called when it is determined that a row should be deleted from the - * ContentProvider. The localCursor is on a table from the local ContentProvider - * and its current position is of the row that should be deleted. The localCursor - * is only guaranteed to contain the BaseColumns.ID column so the implementation - * of deleteRow() must query the database directly if other columns are needed. - * <p> - * It is the responsibility of the implementation of this method to ensure that the cursor - * points to the next row when this method returns, either by calling Cursor.deleteRow() or - * Cursor.next(). - * - * @param localCursor The Cursor into the local table, which points to the row that - * is to be deleted. - */ - public void deleteRow(Cursor localCursor) { - localCursor.deleteRow(); - } - - /** - * After {@link #merge} has completed, this method is called to send - * notifications to {@link android.database.ContentObserver}s of changes - * to the containing {@link ContentProvider}. These notifications likely - * do not want to request a sync back to the network. - */ - protected abstract void notifyChanges(); - - private static boolean findInCursor(Cursor cursor, int column, String id) { - while (!cursor.isAfterLast() && !cursor.isNull(column)) { - int comp = id.compareTo(cursor.getString(column)); - if (comp > 0) { - cursor.moveToNext(); - continue; - } - return comp == 0; - } - return false; - } - - public void onMergeCancelled() { - mIsMergeCancelled = true; - } - - /** - * Carry out a merge of the given diffs, and add the results to - * the given MergeResult. If we are the first merge to find - * client-side diffs, we'll use the given ContentProvider to - * construct a temporary instance to hold them. - */ - public void merge(final SyncContext context, - final Account account, - final SyncableContentProvider serverDiffs, - TempProviderSyncResult result, - SyncResult syncResult, SyncableContentProvider temporaryInstanceFactory) { - mIsMergeCancelled = false; - if (serverDiffs != null) { - if (!mDb.isDbLockedByCurrentThread()) { - throw new IllegalStateException("this must be called from within a DB transaction"); - } - mergeServerDiffs(context, account, serverDiffs, syncResult); - notifyChanges(); - } - - if (result != null) { - findLocalChanges(result, temporaryInstanceFactory, account, syncResult); - } - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "merge complete"); - } - - /** - * @hide this is public for testing purposes only - */ - public void mergeServerDiffs(SyncContext context, - Account account, SyncableContentProvider serverDiffs, SyncResult syncResult) { - boolean diffsArePartial = serverDiffs.getContainsDiffs(); - // mark the current rows so that we can distinguish these from new - // inserts that occur during the merge - mDb.update(mTable, mSyncMarkValues, null, null); - if (mDeletedTable != null) { - mDb.update(mDeletedTable, mSyncMarkValues, null, null); - } - - Cursor localCursor = null; - Cursor deletedCursor = null; - Cursor diffsCursor = null; - try { - // load the local database entries, so we can merge them with the server - final String[] accountSelectionArgs = new String[]{account.name, account.type}; - localCursor = mDb.query(mTable, syncDirtyProjection, - SELECT_MARKED, accountSelectionArgs, null, null, - mTable + "." + _SYNC_ID); - if (mDeletedTable != null) { - deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, - SELECT_MARKED, accountSelectionArgs, null, null, - mDeletedTable + "." + _SYNC_ID); - } else { - deletedCursor = - mDb.rawQuery("select 'a' as _sync_id, 'b' as _sync_version limit 0", null); - } - - // Apply updates and insertions from the server - diffsCursor = serverDiffs.query(mTableURL, - null, null, null, mTable + "." + _SYNC_ID); - int deletedSyncIDColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_ID); - int deletedSyncVersionColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_VERSION); - int serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); - int serverSyncVersionColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_VERSION); - int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - - String lastSyncId = null; - int diffsCount = 0; - int localCount = 0; - localCursor.moveToFirst(); - deletedCursor.moveToFirst(); - while (diffsCursor.moveToNext()) { - if (mIsMergeCancelled) { - return; - } - mDb.yieldIfContended(); - String serverSyncId = diffsCursor.getString(serverSyncIDColumn); - String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn); - long localRowId = 0; - String localSyncVersion = null; - - diffsCount++; - context.setStatusText("Processing " + diffsCount + "/" - + diffsCursor.getCount()); - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processing server entry " + - diffsCount + ", " + serverSyncId); - - if (TRACE) { - if (diffsCount == 10) { - Debug.startMethodTracing("atmtrace"); - } - if (diffsCount == 20) { - Debug.stopMethodTracing(); - } - } - - boolean conflict = false; - boolean update = false; - boolean insert = false; - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "found event with serverSyncID " + serverSyncId); - } - if (TextUtils.isEmpty(serverSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.e(TAG, "server entry doesn't have a serverSyncID"); - } - continue; - } - - // It is possible that the sync adapter wrote the same record multiple times, - // e.g. if the same record came via multiple feeds. If this happens just ignore - // the duplicate records. - if (serverSyncId.equals(lastSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "skipping record with duplicate remote server id " + lastSyncId); - } - continue; - } - lastSyncId = serverSyncId; - - String localSyncID = null; - boolean localSyncDirty = false; - - while (!localCursor.isAfterLast()) { - if (mIsMergeCancelled) { - return; - } - localCount++; - localSyncID = localCursor.getString(2); - - // If the local record doesn't have a _sync_id then - // it is new. Ignore it for now, we will send an insert - // the the server later. - if (TextUtils.isEmpty(localSyncID)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has no _sync_id, ignoring"); - } - localCursor.moveToNext(); - localSyncID = null; - continue; - } - - int comp = serverSyncId.compareTo(localSyncID); - - // the local DB has a record that the server doesn't have - if (comp > 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that is < server _sync_id " + serverSyncId); - } - if (diffsArePartial) { - localCursor.moveToNext(); - } else { - deleteRow(localCursor); - if (mDeletedTable != null) { - mDb.delete(mDeletedTable, _SYNC_ID +"=?", new String[] {localSyncID}); - } - syncResult.stats.numDeletes++; - mDb.yieldIfContended(); - } - localSyncID = null; - continue; - } - - // the server has a record that the local DB doesn't have - if (comp < 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that is > server _sync_id " + serverSyncId); - } - localSyncID = null; - } - - // the server and the local DB both have this record - if (comp == 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that matches the server _sync_id"); - } - localSyncDirty = localCursor.getInt(0) != 0; - localRowId = localCursor.getLong(1); - localSyncVersion = localCursor.getString(3); - localCursor.moveToNext(); - } - - break; - } - - // If this record is in the deleted table then update the server version - // in the deleted table, if necessary, and then ignore it here. - // We will send a deletion indication to the server down a - // little further. - if (findInCursor(deletedCursor, deletedSyncIDColumn, serverSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId + " is in the deleted table"); - } - final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn); - if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "setting version of deleted record " + serverSyncId + " to " - + serverSyncVersion); - } - ContentValues values = new ContentValues(); - values.put(_SYNC_VERSION, serverSyncVersion); - mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId}); - } - continue; - } - - // If the _sync_local_id is present in the diffsCursor - // then this record corresponds to a local record that was just - // inserted into the server and the _sync_local_id is the row id - // of the local record. Set these fields so that the next check - // treats this record as an update, which will allow the - // merger to update the record with the server's sync id - if (!diffsCursor.isNull(serverSyncLocalIdColumn)) { - localRowId = diffsCursor.getLong(serverSyncLocalIdColumn); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the remote record with sync id " + serverSyncId - + " has a local sync id, " + localRowId); - } - localSyncID = serverSyncId; - localSyncDirty = false; - localSyncVersion = null; - } - - if (!TextUtils.isEmpty(localSyncID)) { - // An existing server item has changed - // If serverSyncVersion is null, there is no edit URL; - // server won't let this change be written. - boolean recordChanged = (localSyncVersion == null) || - (serverSyncVersion == null) || - !serverSyncVersion.equals(localSyncVersion); - if (recordChanged) { - if (localSyncDirty) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId - + " conflicts with local _sync_id " + localSyncID - + ", local _id " + localRowId); - } - conflict = true; - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "remote record " + - serverSyncId + - " updates local _sync_id " + - localSyncID + ", local _id " + - localRowId); - } - update = true; - } - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "Skipping update: localSyncVersion: " + localSyncVersion + - ", serverSyncVersion: " + serverSyncVersion); - } - } - } else { - // the local db doesn't know about this record so add it - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId + " is new, inserting"); - } - insert = true; - } - - if (update) { - updateRow(localRowId, serverDiffs, diffsCursor); - syncResult.stats.numUpdates++; - } else if (conflict) { - resolveRow(localRowId, serverSyncId, serverDiffs, diffsCursor); - syncResult.stats.numUpdates++; - } else if (insert) { - insertRow(serverDiffs, diffsCursor); - syncResult.stats.numInserts++; - } - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "processed " + diffsCount + " server entries"); - } - - // If tombstones aren't in use delete any remaining local rows that - // don't have corresponding server rows. Keep the rows that don't - // have a sync id since those were created locally and haven't been - // synced to the server yet. - if (!diffsArePartial) { - while (!localCursor.isAfterLast() && !TextUtils.isEmpty(localCursor.getString(2))) { - if (mIsMergeCancelled) { - return; - } - localCount++; - final String localSyncId = localCursor.getString(2); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "deleting local record " + - localCursor.getLong(1) + - " _sync_id " + localSyncId); - } - deleteRow(localCursor); - if (mDeletedTable != null) { - mDb.delete(mDeletedTable, _SYNC_ID + "=?", new String[] {localSyncId}); - } - syncResult.stats.numDeletes++; - mDb.yieldIfContended(); - } - } - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "checked " + localCount + - " local entries"); - } finally { - if (diffsCursor != null) diffsCursor.close(); - if (localCursor != null) localCursor.close(); - if (deletedCursor != null) deletedCursor.close(); - } - - - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "applying deletions from the server"); - - // Apply deletions from the server - if (mDeletedTableURL != null) { - diffsCursor = serverDiffs.query(mDeletedTableURL, null, null, null, null); - try { - while (diffsCursor.moveToNext()) { - if (mIsMergeCancelled) { - return; - } - // delete all rows that match each element in the diffsCursor - fullyDeleteMatchingRows(diffsCursor, account, syncResult); - mDb.yieldIfContended(); - } - } finally { - diffsCursor.close(); - } - } - } - - private void fullyDeleteMatchingRows(Cursor diffsCursor, Account account, - SyncResult syncResult) { - int serverSyncIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); - final boolean deleteBySyncId = !diffsCursor.isNull(serverSyncIdColumn); - - // delete the rows explicitly so that the delete operation can be overridden - final String[] selectionArgs; - Cursor c = null; - try { - if (deleteBySyncId) { - selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn), - account.name, account.type}; - c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_SYNC_ID_AND_ACCOUNT, - selectionArgs, null, null, null); - } else { - int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - selectionArgs = new String[]{diffsCursor.getString(serverSyncLocalIdColumn)}; - c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_ID, selectionArgs, - null, null, null); - } - c.moveToFirst(); - while (!c.isAfterLast()) { - deleteRow(c); // advances the cursor - syncResult.stats.numDeletes++; - } - } finally { - if (c != null) c.close(); - } - if (deleteBySyncId && mDeletedTable != null) { - mDb.delete(mDeletedTable, SELECT_BY_SYNC_ID_AND_ACCOUNT, selectionArgs); - } - } - - /** - * Converts cursor into a Map, using the correct types for the values. - */ - protected void cursorRowToContentValues(Cursor cursor, ContentValues map) { - DatabaseUtils.cursorRowToContentValues(cursor, map); - } - - /** - * Finds local changes, placing the results in the given result object. - * @param temporaryInstanceFactory As an optimization for the case - * where there are no client-side diffs, mergeResult may initially - * have no {@link TempProviderSyncResult#tempContentProvider}. If this is - * the first in the sequence of AbstractTableMergers to find - * client-side diffs, it will use the given ContentProvider to - * create a temporary instance and store its {@link - * android.content.ContentProvider} in the mergeResult. - * @param account - * @param syncResult - */ - private void findLocalChanges(TempProviderSyncResult mergeResult, - SyncableContentProvider temporaryInstanceFactory, Account account, - SyncResult syncResult) { - SyncableContentProvider clientDiffs = mergeResult.tempContentProvider; - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client updates"); - - final String[] accountSelectionArgs = new String[]{account.name, account.type}; - - // Generate the client updates and insertions - // Create a cursor for dirty records - long numInsertsOrUpdates = 0; - Cursor localChangesCursor = mDb.query(mTable, null, SELECT_UNSYNCED, accountSelectionArgs, - null, null, null); - try { - numInsertsOrUpdates = localChangesCursor.getCount(); - while (localChangesCursor.moveToNext()) { - if (mIsMergeCancelled) { - return; - } - if (clientDiffs == null) { - clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); - } - mValues.clear(); - cursorRowToContentValues(localChangesCursor, mValues); - mValues.remove("_id"); - DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues, - _SYNC_LOCAL_ID); - clientDiffs.insert(mTableURL, mValues); - } - } finally { - localChangesCursor.close(); - } - - // Generate the client deletions - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client deletions"); - long numEntries = DatabaseUtils.queryNumEntries(mDb, mTable); - long numDeletedEntries = 0; - if (mDeletedTable != null) { - Cursor deletedCursor = mDb.query(mDeletedTable, - syncIdAndVersionProjection, - _SYNC_ACCOUNT + "=? AND " + _SYNC_ACCOUNT_TYPE + "=? AND " - + _SYNC_ID + " IS NOT NULL", accountSelectionArgs, - null, null, mDeletedTable + "." + _SYNC_ID); - try { - numDeletedEntries = deletedCursor.getCount(); - while (deletedCursor.moveToNext()) { - if (mIsMergeCancelled) { - return; - } - if (clientDiffs == null) { - clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); - } - mValues.clear(); - DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues); - clientDiffs.insert(mDeletedTableURL, mValues); - } - } finally { - deletedCursor.close(); - } - } - - if (clientDiffs != null) { - mergeResult.tempContentProvider = clientDiffs; - } - syncResult.stats.numDeletes += numDeletedEntries; - syncResult.stats.numUpdates += numInsertsOrUpdates; - syncResult.stats.numEntries += numEntries; - } -} diff --git a/core/java/android/content/AbstractThreadedSyncAdapter.java b/core/java/android/content/AbstractThreadedSyncAdapter.java index 154c4a6..14bc5dd 100644 --- a/core/java/android/content/AbstractThreadedSyncAdapter.java +++ b/core/java/android/content/AbstractThreadedSyncAdapter.java @@ -117,7 +117,7 @@ public abstract class AbstractThreadedSyncAdapter { if (mSyncThread != null && mSyncThread.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { - mSyncThread.interrupt(); + onSyncCanceled(mSyncThread); } } } @@ -207,4 +207,15 @@ public abstract class AbstractThreadedSyncAdapter { */ public abstract void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult); + + /** + * Indicates that a sync operation has been canceled. This will be invoked on a separate + * thread than the sync thread and so you must consider the multi-threaded implications + * of the work that you do in this method. + * + * @param thread the thread that is running the sync operation to cancel + */ + public void onSyncCanceled(Thread thread) { + thread.interrupt(); + } } diff --git a/core/java/android/content/SyncAdapter.java b/core/java/android/content/SyncAdapter.java deleted file mode 100644 index a5afe87..0000000 --- a/core/java/android/content/SyncAdapter.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import android.os.Bundle; -import android.os.RemoteException; -import android.accounts.Account; - -/** - * @hide - */ -public abstract class SyncAdapter { - private static final String TAG = "SyncAdapter"; - - /** Kernel event log tag. */ - public static final int LOG_SYNC_DETAILS = EventLogTags.SYNC_DETAILS; - - class Transport extends ISyncAdapter.Stub { - public void startSync(ISyncContext syncContext, String authority, Account account, - Bundle extras) throws RemoteException { - SyncAdapter.this.startSync(new SyncContext(syncContext), account, authority, extras); - } - - public void cancelSync(ISyncContext syncContext) throws RemoteException { - SyncAdapter.this.cancelSync(); - } - - public void initialize(Account account, String authority) throws RemoteException { - Bundle extras = new Bundle(); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); - startSync(null, authority, account, extras); - } - } - - Transport mTransport = new Transport(); - - /** - * Get the Transport object. - */ - public final ISyncAdapter getISyncAdapter() - { - return mTransport; - } - - /** - * Initiate a sync for this account. SyncAdapter-specific parameters may - * be specified in extras, which is guaranteed to not be null. IPC invocations - * of this method and cancelSync() are guaranteed to be serialized. - * - * @param syncContext the ISyncContext used to indicate the progress of the sync. When - * the sync is finished (successfully or not) ISyncContext.onFinished() must be called. - * @param account the account that should be synced - * @param authority the authority if the sync request - * @param extras SyncAdapter-specific parameters - */ - public abstract void startSync(SyncContext syncContext, Account account, String authority, - Bundle extras); - - /** - * Cancel the most recently initiated sync. Due to race conditions, this may arrive - * after the ISyncContext.onFinished() for that sync was called. IPC invocations - * of this method and startSync() are guaranteed to be serialized. - */ - public abstract void cancelSync(); -} diff --git a/core/java/android/content/SyncStateContentProviderHelper.java b/core/java/android/content/SyncStateContentProviderHelper.java deleted file mode 100644 index 64bbe25..0000000 --- a/core/java/android/content/SyncStateContentProviderHelper.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import com.android.internal.util.ArrayUtils; - -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; -import android.accounts.Account; - -/** - * Extends the schema of a ContentProvider to include the _sync_state table - * and implements query/insert/update/delete to access that table using the - * authority "syncstate". This can be used to store the sync state for a - * set of accounts. - * - * @hide - */ -public class SyncStateContentProviderHelper { - final SQLiteOpenHelper mOpenHelper; - - private static final String SYNC_STATE_AUTHORITY = "syncstate"; - private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); - - private static final int STATE = 0; - - private static final Uri CONTENT_URI = - Uri.parse("content://" + SYNC_STATE_AUTHORITY + "/state"); - - private static final String ACCOUNT_WHERE = "_sync_account = ? AND _sync_account_type = ?"; - - private final Provider mInternalProviderInterface; - - private static final String SYNC_STATE_TABLE = "_sync_state"; - private static long DB_VERSION = 3; - - private static final String[] ACCOUNT_PROJECTION = - new String[]{"_sync_account", "_sync_account_type"}; - - static { - sURIMatcher.addURI(SYNC_STATE_AUTHORITY, "state", STATE); - } - - public SyncStateContentProviderHelper(SQLiteOpenHelper openHelper) { - mOpenHelper = openHelper; - mInternalProviderInterface = new Provider(); - } - - public ContentProvider asContentProvider() { - return mInternalProviderInterface; - } - - public void createDatabase(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS _sync_state"); - db.execSQL("CREATE TABLE _sync_state (" + - "_id INTEGER PRIMARY KEY," + - "_sync_account TEXT," + - "_sync_account_type TEXT," + - "data TEXT," + - "UNIQUE(_sync_account, _sync_account_type)" + - ");"); - - db.execSQL("DROP TABLE IF EXISTS _sync_state_metadata"); - db.execSQL("CREATE TABLE _sync_state_metadata (" + - "version INTEGER" + - ");"); - ContentValues values = new ContentValues(); - values.put("version", DB_VERSION); - db.insert("_sync_state_metadata", "version", values); - } - - protected void onDatabaseOpened(SQLiteDatabase db) { - long version = DatabaseUtils.longForQuery(db, - "select version from _sync_state_metadata", null); - if (version != DB_VERSION) { - createDatabase(db); - } - } - - class Provider extends ContentProvider { - public boolean onCreate() { - throw new UnsupportedOperationException("not implemented"); - } - - public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); - int match = sURIMatcher.match(url); - switch (match) { - case STATE: - return db.query(SYNC_STATE_TABLE, projection, selection, selectionArgs, - null, null, sortOrder); - default: - throw new UnsupportedOperationException("Cannot query URL: " + url); - } - } - - public String getType(Uri uri) { - throw new UnsupportedOperationException("not implemented"); - } - - public Uri insert(Uri url, ContentValues values) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - int match = sURIMatcher.match(url); - switch (match) { - case STATE: { - long id = db.insert(SYNC_STATE_TABLE, "feed", values); - return CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); - } - default: - throw new UnsupportedOperationException("Cannot insert into URL: " + url); - } - } - - public int delete(Uri url, String userWhere, String[] whereArgs) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - switch (sURIMatcher.match(url)) { - case STATE: - return db.delete(SYNC_STATE_TABLE, userWhere, whereArgs); - default: - throw new IllegalArgumentException("Unknown URL " + url); - } - - } - - public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - switch (sURIMatcher.match(url)) { - case STATE: - return db.update(SYNC_STATE_TABLE, values, selection, selectionArgs); - default: - throw new UnsupportedOperationException("Cannot update URL: " + url); - } - - } - } - - /** - * Check if the url matches content that this ContentProvider manages. - * @param url the Uri to check - * @return true if this ContentProvider can handle that Uri. - */ - public boolean matches(Uri url) { - return (SYNC_STATE_AUTHORITY.equals(url.getAuthority())); - } - - /** - * Replaces the contents of the _sync_state table in the destination ContentProvider - * with the row that matches account, if any, in the source ContentProvider. - * <p> - * The ContentProviders must expose the _sync_state table as URI content://syncstate/state. - * @param dbSrc the database to read from - * @param dbDest the database to write to - * @param account the account of the row that should be copied over. - */ - public void copySyncState(SQLiteDatabase dbSrc, SQLiteDatabase dbDest, - Account account) { - final String[] whereArgs = new String[]{account.name, account.type}; - Cursor c = dbSrc.query(SYNC_STATE_TABLE, - new String[]{"_sync_account", "_sync_account_type", "data"}, - ACCOUNT_WHERE, whereArgs, null, null, null); - try { - if (c.moveToNext()) { - ContentValues values = new ContentValues(); - values.put("_sync_account", c.getString(0)); - values.put("_sync_account_type", c.getString(1)); - values.put("data", c.getBlob(2)); - dbDest.replace(SYNC_STATE_TABLE, "_sync_account", values); - } - } finally { - c.close(); - } - } - - public void onAccountsChanged(Account[] accounts) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - Cursor c = db.query(SYNC_STATE_TABLE, ACCOUNT_PROJECTION, null, null, null, null, null); - try { - while (c.moveToNext()) { - final String accountName = c.getString(0); - final String accountType = c.getString(1); - Account account = new Account(accountName, accountType); - if (!ArrayUtils.contains(accounts, account)) { - db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, - new String[]{accountName, accountType}); - } - } - } finally { - c.close(); - } - } - - public void discardSyncData(SQLiteDatabase db, Account account) { - if (account != null) { - db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account.name, account.type}); - } else { - db.delete(SYNC_STATE_TABLE, null, null); - } - } - - /** - * Retrieves the SyncData bytes for the given account. The byte array returned may be null. - */ - public byte[] readSyncDataBytes(SQLiteDatabase db, Account account) { - Cursor c = db.query(SYNC_STATE_TABLE, null, ACCOUNT_WHERE, - new String[]{account.name, account.type}, null, null, null); - try { - if (c.moveToFirst()) { - return c.getBlob(c.getColumnIndexOrThrow("data")); - } - } finally { - c.close(); - } - return null; - } - - /** - * Sets the SyncData bytes for the given account. The bytes array may be null. - */ - public void writeSyncDataBytes(SQLiteDatabase db, Account account, byte[] data) { - ContentValues values = new ContentValues(); - values.put("data", data); - db.update(SYNC_STATE_TABLE, values, ACCOUNT_WHERE, - new String[]{account.name, account.type}); - } -} diff --git a/core/java/android/content/SyncableContentProvider.java b/core/java/android/content/SyncableContentProvider.java deleted file mode 100644 index ab4e91c..0000000 --- a/core/java/android/content/SyncableContentProvider.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.accounts.Account; - -import java.util.Map; - -/** - * 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 SyncableContentProvider extends ContentProvider { - protected abstract boolean isTemporary(); - - private volatile TempProviderSyncAdapter mTempProviderSyncAdapter; - - public void setTempProviderSyncAdapter(TempProviderSyncAdapter syncAdapter) { - mTempProviderSyncAdapter = syncAdapter; - } - - public TempProviderSyncAdapter getTempProviderSyncAdapter() { - return mTempProviderSyncAdapter; - } - - /** - * Close resources that must be closed. You must call this to properly release - * the resources used by the SyncableContentProvider. - */ - public abstract void close(); - - /** - * 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 abstract 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 abstract void onDatabaseOpened(SQLiteDatabase db); - - /** - * 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 abstract SyncableContentProvider getTemporaryInstance(); - - public abstract SQLiteDatabase getDatabase(); - - public abstract boolean getContainsDiffs(); - - public abstract void setContainsDiffs(boolean containsDiffs); - - /** - * Each subclass of this class should define a subclass of {@link - * 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 - * 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 - * AbstractTableMerger}, one for each table that should be merged. - */ - protected abstract Iterable<? extends AbstractTableMerger> getMergers(); - - /** - * 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 abstract boolean changeRequiresLocalSync(Uri uri); - - /** - * Called right before a sync is started. - * - * @param context the sync context for the operation - * @param account - */ - public abstract void onSyncStart(SyncContext context, Account 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 abstract void onSyncStop(SyncContext context, boolean success); - - /** - * The account of the most recent call to onSyncStart() - * @return the account - */ - public abstract Account getSyncingAccount(); - - /** - * 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 abstract void merge(SyncContext context, SyncableContentProvider diffs, - TempProviderSyncResult result, SyncResult syncResult); - - - /** - * Invoked when the active sync has been canceled. The default - * implementation doesn't do anything (except ensure that this - * provider is syncable). Subclasses of ContentProvider - * that support canceling of sync should override this. - */ - public abstract void onSyncCanceled(); - - - public abstract boolean isMergeCancelled(); - - /** - * 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 abstract void onAccountsChanged(Account[] accountsArray); - - /** - * 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 - */ - protected abstract void deleteRowsForRemovedAccounts(Map<Account, Boolean> accounts, - String table); - - /** - * Called when the sync system determines that this provider should no longer - * contain records for the specified account. - */ - public abstract void wipeAccount(Account account); - - /** - * Retrieves the SyncData bytes for the given account. The byte array returned may be null. - */ - public abstract byte[] readSyncDataBytes(Account account); - - /** - * Sets the SyncData bytes for the given account. The bytes array may be null. - */ - public abstract void writeSyncDataBytes(Account account, byte[] data); -} - diff --git a/core/java/android/content/TempProviderSyncAdapter.java b/core/java/android/content/TempProviderSyncAdapter.java deleted file mode 100644 index 5ccaa26..0000000 --- a/core/java/android/content/TempProviderSyncAdapter.java +++ /dev/null @@ -1,585 +0,0 @@ -package android.content; - -import android.accounts.Account; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.database.SQLException; -import android.net.TrafficStats; -import android.os.Bundle; -import android.os.Debug; -import android.os.Parcelable; -import android.os.Process; -import android.os.SystemProperties; -import android.text.TextUtils; -import android.util.Config; -import android.util.EventLog; -import android.util.Log; -import android.util.TimingLogger; - -import java.io.IOException; - -/** - * @hide - */ -public abstract class TempProviderSyncAdapter extends SyncAdapter { - private static final String TAG = "Sync"; - - private static final int MAX_GET_SERVER_DIFFS_LOOP_COUNT = 20; - private static final int MAX_UPLOAD_CHANGES_LOOP_COUNT = 10; - private static final int NUM_ALLOWED_SIMULTANEOUS_DELETIONS = 5; - private static final long PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS = 20; - - private volatile SyncableContentProvider mProvider; - private volatile SyncThread mSyncThread = null; - private volatile boolean mProviderSyncStarted; - private volatile boolean mAdapterSyncStarted; - - public TempProviderSyncAdapter(SyncableContentProvider provider) { - super(); - mProvider = provider; - } - - /** - * Used by getServerDiffs() to track the sync progress for a given - * sync adapter. Implementations of SyncAdapter generally specialize - * this class in order to track specific data about that SyncAdapter's - * sync. If an implementation of SyncAdapter doesn't need to store - * any data for a sync it may use TrivialSyncData. - */ - public static abstract class SyncData implements Parcelable { - - } - - public final void setContext(Context context) { - mContext = context; - } - - /** - * Retrieve the Context this adapter is running in. Only available - * once onSyncStarting() is called (not available from constructor). - */ - final public Context getContext() { - return mContext; - } - - /** - * Called right before a sync is started. - * - * @param context allows you to publish status and interact with the - * @param account the account to sync - * @param manualSync true if this sync was requested manually by the user - * @param result information to track what happened during this sync attempt - */ - public abstract void onSyncStarting(SyncContext context, Account account, boolean manualSync, - SyncResult result); - - /** - * Called right after a sync is completed - * - * @param context allows you to publish status and interact with the - * user during interactive syncs. - * @param success true if the sync suceeded, false if an error occured - */ - public abstract void onSyncEnding(SyncContext context, boolean success); - - /** - * Implement this to return true if the data in your content provider - * is read only. - */ - public abstract boolean isReadOnly(); - - public abstract boolean getIsSyncable(Account account) - throws IOException, AuthenticatorException, OperationCanceledException; - - /** - * Get diffs from the server since the last completed sync and put them - * into a temporary provider. - * - * @param context allows you to publish status and interact with the - * user during interactive syncs. - * @param syncData used to track the progress this client has made in syncing data - * from the server - * @param tempProvider this is where the diffs should be stored - * @param extras any extra data describing the sync that is desired - * @param syncInfo sync adapter-specific data that is used during a single sync operation - * @param syncResult information to track what happened during this sync attempt - */ - public abstract void getServerDiffs(SyncContext context, - SyncData syncData, SyncableContentProvider tempProvider, - Bundle extras, Object syncInfo, SyncResult syncResult); - - /** - * Send client diffs to the server, optionally receiving more diffs from the server - * - * @param context allows you to publish status and interact with the - * user during interactive syncs. - * @param clientDiffs the diffs from the client - * @param serverDiffs the SyncableContentProvider that should be populated with -* the entries that were returned in response to an insert/update/delete request -* to the server - * @param syncResult information to track what happened during this sync attempt - * @param dontActuallySendDeletes - */ - public abstract void sendClientDiffs(SyncContext context, - SyncableContentProvider clientDiffs, - SyncableContentProvider serverDiffs, SyncResult syncResult, - boolean dontActuallySendDeletes); - - /** - * Reads the sync data from the ContentProvider - * @param contentProvider the ContentProvider to read from - * @return the SyncData for the provider. This may be null. - */ - public SyncData readSyncData(SyncableContentProvider contentProvider) { - return null; - } - - /** - * Create and return a new, empty SyncData object - */ - public SyncData newSyncData() { - return null; - } - - /** - * Stores the sync data in the Sync Stats database, keying it by - * the account that was set in the last call to onSyncStarting() - */ - public void writeSyncData(SyncData syncData, SyncableContentProvider contentProvider) {} - - /** - * Indicate to the SyncAdapter that the last sync that was started has - * been cancelled. - */ - public abstract void onSyncCanceled(); - - /** - * Initializes the temporary content providers used during - * {@link TempProviderSyncAdapter#sendClientDiffs}. - * May copy relevant data from the underlying db into this provider so - * joins, etc., can work. - * - * @param cp The ContentProvider to initialize. - */ - protected void initTempProvider(SyncableContentProvider cp) {} - - protected Object createSyncInfo() { - return null; - } - - /** - * Called when the accounts list possibly changed, to give the - * SyncAdapter a chance to do any necessary bookkeeping, e.g. - * to make sure that any required SubscribedFeeds subscriptions - * exist. - * @param accounts the list of accounts - */ - public abstract void onAccountsChanged(Account[] accounts); - - private Context mContext; - - private class SyncThread extends Thread { - private final Account mAccount; - private final String mAuthority; - private final Bundle mExtras; - private final SyncContext mSyncContext; - private volatile boolean mIsCanceled = false; - private long mInitialTxBytes; - private long mInitialRxBytes; - private final SyncResult mResult; - - SyncThread(SyncContext syncContext, Account account, String authority, Bundle extras) { - super("SyncThread"); - mAccount = account; - mAuthority = authority; - mExtras = extras; - mSyncContext = syncContext; - mResult = new SyncResult(); - } - - void cancelSync() { - mIsCanceled = true; - if (mAdapterSyncStarted) onSyncCanceled(); - if (mProviderSyncStarted) mProvider.onSyncCanceled(); - // We may lose the last few sync events when canceling. Oh well. - int uid = Process.myUid(); - logSyncDetails(TrafficStats.getUidTxBytes(uid) - mInitialTxBytes, - TrafficStats.getUidRxBytes(uid) - mInitialRxBytes, mResult); - } - - @Override - public void run() { - Process.setThreadPriority(Process.myTid(), - Process.THREAD_PRIORITY_BACKGROUND); - int uid = Process.myUid(); - mInitialTxBytes = TrafficStats.getUidTxBytes(uid); - mInitialRxBytes = TrafficStats.getUidRxBytes(uid); - try { - sync(mSyncContext, mAccount, mAuthority, mExtras); - } catch (SQLException e) { - Log.e(TAG, "Sync failed", e); - mResult.databaseError = true; - } finally { - mSyncThread = null; - if (!mIsCanceled) { - logSyncDetails(TrafficStats.getUidTxBytes(uid) - mInitialTxBytes, - TrafficStats.getUidRxBytes(uid) - mInitialRxBytes, mResult); - mSyncContext.onFinished(mResult); - } - } - } - - private void sync(SyncContext syncContext, Account account, String authority, - Bundle extras) { - mIsCanceled = false; - - mProviderSyncStarted = false; - mAdapterSyncStarted = false; - String message = null; - - // always attempt to initialize if the isSyncable state isn't set yet - int isSyncable = ContentResolver.getIsSyncable(account, authority); - if (isSyncable < 0) { - try { - isSyncable = (getIsSyncable(account)) ? 1 : 0; - ContentResolver.setIsSyncable(account, authority, isSyncable); - } catch (IOException e) { - ++mResult.stats.numIoExceptions; - } catch (AuthenticatorException e) { - ++mResult.stats.numParseExceptions; - } catch (OperationCanceledException e) { - // do nothing - } - } - - // if this is an initialization request then our work is done here - if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { - return; - } - - // if we aren't syncable then get out - if (isSyncable <= 0) { - return; - } - - boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); - - try { - mProvider.onSyncStart(syncContext, account); - mProviderSyncStarted = true; - onSyncStarting(syncContext, account, manualSync, mResult); - if (mResult.hasError()) { - message = "SyncAdapter failed while trying to start sync"; - return; - } - mAdapterSyncStarted = true; - if (mIsCanceled) { - return; - } - final String syncTracingEnabledValue = SystemProperties.get(TAG + "Tracing"); - final boolean syncTracingEnabled = !TextUtils.isEmpty(syncTracingEnabledValue); - try { - if (syncTracingEnabled) { - System.gc(); - System.gc(); - Debug.startMethodTracing("synctrace." + System.currentTimeMillis()); - } - runSyncLoop(syncContext, account, extras); - } finally { - if (syncTracingEnabled) Debug.stopMethodTracing(); - } - onSyncEnding(syncContext, !mResult.hasError()); - mAdapterSyncStarted = false; - mProvider.onSyncStop(syncContext, true); - mProviderSyncStarted = false; - } finally { - if (mAdapterSyncStarted) { - mAdapterSyncStarted = false; - onSyncEnding(syncContext, false); - } - if (mProviderSyncStarted) { - mProviderSyncStarted = false; - mProvider.onSyncStop(syncContext, false); - } - if (!mIsCanceled) { - if (message != null) syncContext.setStatusText(message); - } - } - } - - private void runSyncLoop(SyncContext syncContext, Account account, Bundle extras) { - TimingLogger syncTimer = new TimingLogger(TAG + "Profiling", "sync"); - syncTimer.addSplit("start"); - int loopCount = 0; - boolean tooManyGetServerDiffsAttempts = false; - - final boolean overrideTooManyDeletions = - extras.getBoolean(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS, - false); - final boolean discardLocalDeletions = - extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS, false); - boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, - false /* default this flag to false */); - SyncableContentProvider serverDiffs = null; - TempProviderSyncResult result = new TempProviderSyncResult(); - try { - if (!uploadOnly) { - /** - * This loop repeatedly calls SyncAdapter.getServerDiffs() - * (to get changes from the feed) followed by - * ContentProvider.merge() (to incorporate these changes - * into the provider), stopping when the SyncData returned - * from getServerDiffs() indicates that all the data was - * fetched. - */ - while (!mIsCanceled) { - // Don't let a bad sync go forever - if (loopCount++ == MAX_GET_SERVER_DIFFS_LOOP_COUNT) { - Log.e(TAG, "runSyncLoop: Hit max loop count while getting server diffs " - + getClass().getName()); - // TODO: change the structure here to schedule a new sync - // with a backoff time, keeping track to be sure - // we don't keep doing this forever (due to some bug or - // mismatch between the client and the server) - tooManyGetServerDiffsAttempts = true; - break; - } - - // Get an empty content provider to put the diffs into - if (serverDiffs != null) serverDiffs.close(); - serverDiffs = mProvider.getTemporaryInstance(); - - // Get records from the server which will be put into the serverDiffs - initTempProvider(serverDiffs); - Object syncInfo = createSyncInfo(); - SyncData syncData = readSyncData(serverDiffs); - // syncData will only be null if there was a demarshalling error - // while reading the sync data. - if (syncData == null) { - mProvider.wipeAccount(account); - syncData = newSyncData(); - } - mResult.clear(); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: running getServerDiffs using syncData " - + syncData.toString()); - } - getServerDiffs(syncContext, syncData, serverDiffs, extras, syncInfo, - mResult); - - if (mIsCanceled) return; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: result: " + mResult); - } - if (mResult.hasError()) return; - if (mResult.partialSyncUnavailable) { - if (Config.LOGD) { - Log.d(TAG, "partialSyncUnavailable is set, setting " - + "ignoreSyncData and retrying"); - } - mProvider.wipeAccount(account); - continue; - } - - // write the updated syncData back into the temp provider - writeSyncData(syncData, serverDiffs); - - // apply the downloaded changes to the provider - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: running merge"); - } - mProvider.merge(syncContext, serverDiffs, - null /* don't return client diffs */, mResult); - if (mIsCanceled) return; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: result: " + mResult); - } - - // if the server has no more changes then break out of the loop - if (!mResult.moreRecordsToGet) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: fetched all data, moving on"); - } - break; - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: more data to fetch, looping"); - } - } - } - - /** - * This loop repeatedly calls ContentProvider.merge() followed - * by SyncAdapter.merge() until either indicate that there is - * no more work to do by returning null. - * <p> - * The initial ContentProvider.merge() returns a temporary - * ContentProvider that contains any local changes that need - * to be committed to the server. - * <p> - * The SyncAdapter.merge() calls upload the changes to the server - * and populates temporary provider (the serverDiffs) with the - * result. - * <p> - * Subsequent calls to ContentProvider.merge() incoporate the - * result of previous SyncAdapter.merge() calls into the - * real ContentProvider and again return a temporary - * ContentProvider that contains any local changes that need - * to be committed to the server. - */ - loopCount = 0; - boolean readOnly = isReadOnly(); - long previousNumModifications = 0; - if (serverDiffs != null) { - serverDiffs.close(); - serverDiffs = null; - } - - // If we are discarding local deletions then we need to redownload all the items - // again (since some of them might have been deleted). We do this by deleting the - // sync data for the current account by writing in a null one. - if (discardLocalDeletions) { - serverDiffs = mProvider.getTemporaryInstance(); - initTempProvider(serverDiffs); - writeSyncData(null, serverDiffs); - } - - while (!mIsCanceled) { - if (Config.LOGV) { - Log.v(TAG, "runSyncLoop: Merging diffs from server to client"); - } - if (result.tempContentProvider != null) { - result.tempContentProvider.close(); - result.tempContentProvider = null; - } - mResult.clear(); - mProvider.merge(syncContext, serverDiffs, readOnly ? null : result, - mResult); - if (mIsCanceled) return; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: result: " + mResult); - } - - SyncableContentProvider clientDiffs = - readOnly ? null : result.tempContentProvider; - if (clientDiffs == null) { - // Nothing to commit back to the server - if (Config.LOGV) Log.v(TAG, "runSyncLoop: No client diffs"); - break; - } - - long numModifications = mResult.stats.numUpdates - + mResult.stats.numDeletes - + mResult.stats.numInserts; - - // as long as we are making progress keep resetting the loop count - if (numModifications < previousNumModifications) { - loopCount = 0; - } - previousNumModifications = numModifications; - - // Don't let a bad sync go forever - if (loopCount++ >= MAX_UPLOAD_CHANGES_LOOP_COUNT) { - Log.e(TAG, "runSyncLoop: Hit max loop count while syncing " - + getClass().getName()); - mResult.tooManyRetries = true; - break; - } - - if (!overrideTooManyDeletions && !discardLocalDeletions - && hasTooManyDeletions(mResult.stats)) { - if (Config.LOGD) { - Log.d(TAG, "runSyncLoop: Too many deletions were found in provider " - + getClass().getName() + ", not doing any more updates"); - } - long numDeletes = mResult.stats.numDeletes; - mResult.stats.clear(); - mResult.tooManyDeletions = true; - mResult.stats.numDeletes = numDeletes; - break; - } - - if (Config.LOGV) Log.v(TAG, "runSyncLoop: Merging diffs from client to server"); - if (serverDiffs != null) serverDiffs.close(); - serverDiffs = clientDiffs.getTemporaryInstance(); - initTempProvider(serverDiffs); - mResult.clear(); - sendClientDiffs(syncContext, clientDiffs, serverDiffs, mResult, - discardLocalDeletions); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: result: " + mResult); - } - - if (!mResult.madeSomeProgress()) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: No data from client diffs merge"); - } - break; - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: made some progress, looping"); - } - } - - // add in any status codes that we saved from earlier - mResult.tooManyRetries |= tooManyGetServerDiffsAttempts; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: final result: " + mResult); - } - } finally { - // do this in the finally block to guarantee that is is set and not overwritten - if (discardLocalDeletions) { - mResult.fullSyncRequested = true; - } - if (serverDiffs != null) serverDiffs.close(); - if (result.tempContentProvider != null) result.tempContentProvider.close(); - syncTimer.addSplit("stop"); - syncTimer.dumpToLog(); - } - } - } - - /** - * Logs details on the sync. - * Normally this will be overridden by a subclass that will provide - * provider-specific details. - * - * @param bytesSent number of bytes the sync sent over the network - * @param bytesReceived number of bytes the sync received over the network - * @param result The SyncResult object holding info on the sync - */ - protected void logSyncDetails(long bytesSent, long bytesReceived, SyncResult result) { - EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, ""); - } - - public void startSync(SyncContext syncContext, Account account, String authority, - Bundle extras) { - if (mSyncThread != null) { - syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS); - return; - } - - mSyncThread = new SyncThread(syncContext, account, authority, extras); - mSyncThread.start(); - } - - public void cancelSync() { - if (mSyncThread != null) { - mSyncThread.cancelSync(); - } - } - - protected boolean hasTooManyDeletions(SyncStats stats) { - long numEntries = stats.numEntries; - long numDeletedEntries = stats.numDeletes; - - long percentDeleted = (numDeletedEntries == 0) - ? 0 - : (100 * numDeletedEntries / - (numEntries + numDeletedEntries)); - boolean tooManyDeletions = - (numDeletedEntries > NUM_ALLOWED_SIMULTANEOUS_DELETIONS) - && (percentDeleted > PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS); - return tooManyDeletions; - } -} diff --git a/core/java/android/content/TempProviderSyncResult.java b/core/java/android/content/TempProviderSyncResult.java deleted file mode 100644 index 81f6f79..0000000 --- a/core/java/android/content/TempProviderSyncResult.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -/** - * Used to hold data returned from a given phase of a TempProviderSync. - * @hide - */ -public class TempProviderSyncResult { - /** - * An interface to a temporary content provider that contains - * the result of updates that were sent to the server. This - * provider must be merged into the permanent content provider. - * This may be null, which indicates that there is nothing to - * merge back into the content provider. - */ - public SyncableContentProvider tempContentProvider; - - public TempProviderSyncResult() { - tempContentProvider = null; - } -} diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 117de15..b5c9900 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -30,6 +30,7 @@ import android.content.Entity; import android.content.res.Resources; import android.database.Cursor; import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteException; import android.graphics.Rect; import android.net.Uri; import android.os.RemoteException; @@ -1447,7 +1448,11 @@ public final class ContactsContract { if (cursor.isNull(columnIndex)) { // don't put anything } else { - cv.put(key, cursor.getString(columnIndex)); + try { + cv.put(key, cursor.getString(columnIndex)); + } catch (SQLiteException e) { + cv.put(key, cursor.getBlob(columnIndex)); + } } // TODO: go back to this version of the code when bug // http://b/issue?id=2306370 is fixed. diff --git a/preloaded-classes b/preloaded-classes index ff66183..1b9a310 100644 --- a/preloaded-classes +++ b/preloaded-classes @@ -61,9 +61,6 @@ android.app.SearchDialog$SearchAutoComplete android.app.Service android.app.ServiceConnectionLeaked android.app.TabActivity -android.content.AbstractSyncableContentProvider -android.content.AbstractTableMerger -android.content.AsyncQueryHandler$WorkerHandler android.content.BroadcastReceiver android.content.ComponentCallbacks android.content.ComponentName @@ -87,13 +84,8 @@ android.content.Intent android.content.Intent$1 android.content.IntentFilter android.content.SearchRecentSuggestionsProvider -android.content.ServiceConnection -android.content.SharedPreferences android.content.SyncResult -android.content.SyncResult$1 android.content.SyncStats -android.content.SyncStats$1 -android.content.SyncableContentProvider android.content.UriMatcher android.content.pm.ActivityInfo android.content.pm.ActivityInfo$1 diff --git a/tests/FrameworkTest/tests/src/android/content/AbstractTableMergerTest.java b/tests/FrameworkTest/tests/src/android/content/AbstractTableMergerTest.java deleted file mode 100644 index a8af7f8..0000000 --- a/tests/FrameworkTest/tests/src/android/content/AbstractTableMergerTest.java +++ /dev/null @@ -1,587 +0,0 @@ -package android.content; - -import com.google.android.collect.Lists; -import com.google.android.collect.Sets; - -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.test.AndroidTestCase; -import android.text.TextUtils; -import android.accounts.Account; - -import java.util.ArrayList; -import java.util.Map; -import java.util.SortedSet; - -/** Unit test for {@link android.content.AbstractTableMerger}. */ -public class AbstractTableMergerTest extends AndroidTestCase { - MockSyncableContentProvider mRealProvider; - MockSyncableContentProvider mTempProvider; - MockTableMerger mMerger; - MockSyncContext mSyncContext; - - static final String TABLE_NAME = "items"; - static final String DELETED_TABLE_NAME = "deleted_items"; - static final Uri CONTENT_URI = Uri.parse("content://testdata"); - static final Uri TABLE_URI = Uri.withAppendedPath(CONTENT_URI, TABLE_NAME); - static final Uri DELETED_TABLE_URI = Uri.withAppendedPath(CONTENT_URI, DELETED_TABLE_NAME); - - private final Account ACCOUNT = new Account("account@goo.com", "example.type"); - - private final ArrayList<Expectation> mExpectations = Lists.newArrayList(); - - static class Expectation { - enum Type { - UPDATE, - INSERT, - DELETE, - RESOLVE - } - - Type mType; - ContentValues mValues; - Long mLocalRowId; - - Expectation(Type type, Long localRowId, ContentValues values) { - mType = type; - mValues = values; - mLocalRowId = localRowId; - if (type == Type.DELETE) { - assertNull(values); - } else { - assertFalse(values.containsKey("_id")); - } - } - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - mSyncContext = new MockSyncContext(); - mRealProvider = new MockSyncableContentProvider(); - mTempProvider = mRealProvider.getTemporaryInstance(); - mMerger = new MockTableMerger(mRealProvider.getDatabase(), - TABLE_NAME, TABLE_URI, DELETED_TABLE_NAME, DELETED_TABLE_URI); - mExpectations.clear(); - } - - ContentValues newValues(String data, String syncId, Account syncAccount, - String syncTime, String syncVersion, Long syncLocalId) { - ContentValues values = new ContentValues(); - if (data != null) values.put("data", data); - if (syncTime != null) values.put("_sync_time", syncTime); - if (syncVersion != null) values.put("_sync_version", syncVersion); - if (syncId != null) values.put("_sync_id", syncId); - if (syncAccount != null) { - values.put("_sync_account", syncAccount.name); - values.put("_sync_account_type", syncAccount.type); - } - values.put("_sync_local_id", syncLocalId); - values.put("_sync_dirty", 0); - return values; - } - - ContentValues newDeletedValues(String syncId, Account syncAccount, String syncVersion, - Long syncLocalId) { - ContentValues values = new ContentValues(); - if (syncVersion != null) values.put("_sync_version", syncVersion); - if (syncId != null) values.put("_sync_id", syncId); - if (syncAccount != null) { - values.put("_sync_account", syncAccount.name); - values.put("_sync_account_type", syncAccount.type); - } - if (syncLocalId != null) values.put("_sync_local_id", syncLocalId); - return values; - } - - ContentValues newModifyData(String data) { - ContentValues values = new ContentValues(); - values.put("data", data); - values.put("_sync_dirty", 1); - return values; - } - - // Want to test adding, changing, deleting entries to a provider that has extra entries - // before and after the entries being changed. - public void testInsert() { - // add rows to the real provider - // add new row to the temp provider - final ContentValues row1 = newValues("d1", "si1", ACCOUNT, "st1", "sv1", null); - mTempProvider.insert(TABLE_URI, row1); - - // add expected callbacks to merger - mExpectations.add(new Expectation(Expectation.Type.INSERT, null /* syncLocalId */, row1)); - - // run merger - SyncResult syncResult = new SyncResult(); - mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); - - // check that all expectations were met - assertEquals("not all expectations were met", 0, mExpectations.size()); - } - - public void testUpdateWithLocalId() { - // add rows to the real provider - // add new row to the temp provider that matches an unsynced row in the real provider - final ContentValues row1 = newValues("d1", "si1", ACCOUNT, "st1", "sv1", 11L); - mTempProvider.insert(TABLE_URI, row1); - - // add expected callbacks to merger - mExpectations.add(new Expectation(Expectation.Type.UPDATE, 11L, row1)); - - // run merger - SyncResult syncResult = new SyncResult(); - mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); - - // check that all expectations were met - assertEquals("not all expectations were met", 0, mExpectations.size()); - } - - public void testUpdateWithoutLocalId() { - // add rows to the real provider - Uri i1 = mRealProvider.insert(TABLE_URI, - newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); - - // add new row to the temp provider that matches an unsynced row in the real provider - final ContentValues row1 = newValues("d2", "si1", ACCOUNT, "st2", "sv2", null); - mTempProvider.insert(TABLE_URI, row1); - - // add expected callbacks to merger - mExpectations.add(new Expectation(Expectation.Type.UPDATE, ContentUris.parseId(i1), row1)); - - // run merger - SyncResult syncResult = new SyncResult(); - mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); - - // check that all expectations were met - assertEquals("not all expectations were met", 0, mExpectations.size()); - } - - public void testResolve() { - // add rows to the real provider - Uri i1 = mRealProvider.insert(TABLE_URI, - newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); - mRealProvider.update(TABLE_URI, newModifyData("d2"), null, null); - - // add row to the temp provider that matches a dirty, synced row in the real provider - final ContentValues row1 = newValues("d3", "si1", ACCOUNT, "st2", "sv2", null); - mTempProvider.insert(TABLE_URI, row1); - - // add expected callbacks to merger - mExpectations.add(new Expectation(Expectation.Type.RESOLVE, ContentUris.parseId(i1), row1)); - - // run merger - SyncResult syncResult = new SyncResult(); - mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); - - // check that all expectations were met - assertEquals("not all expectations were met", 0, mExpectations.size()); - } - - public void testResolveWithLocalId() { - // add rows to the real provider - Uri i1 = mRealProvider.insert(TABLE_URI, - newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); - mRealProvider.update(TABLE_URI, newModifyData("d2"), null, null); - - // add row to the temp provider that matches a dirty, synced row in the real provider - ContentValues row1 = newValues("d2", "si1", ACCOUNT, "st2", "sv2", ContentUris.parseId(i1)); - mTempProvider.insert(TABLE_URI, row1); - - // add expected callbacks to merger - mExpectations.add(new Expectation(Expectation.Type.UPDATE, ContentUris.parseId(i1), row1)); - - // run merger - SyncResult syncResult = new SyncResult(); - mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); - - // check that all expectations were met - assertEquals("not all expectations were met", 0, mExpectations.size()); - } - - public void testDeleteRowAfterDelete() { - // add rows to the real provider - Uri i1 = mRealProvider.insert(TABLE_URI, - newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); - - // add a deleted record to the temp provider - ContentValues row1 = newDeletedValues(null, null, null, ContentUris.parseId(i1)); - mTempProvider.insert(DELETED_TABLE_URI, row1); - - // add expected callbacks to merger - mExpectations.add(new Expectation(Expectation.Type.DELETE, ContentUris.parseId(i1), null)); - - // run merger - SyncResult syncResult = new SyncResult(); - mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); - - // check that all expectations were met - assertEquals("not all expectations were met", 0, mExpectations.size()); - } - - public void testDeleteRowAfterInsert() { - // add rows to the real provider - Uri i1 = mRealProvider.insert(TABLE_URI, newModifyData("d1")); - - // add a deleted record to the temp provider - ContentValues row1 = newDeletedValues(null, null, null, ContentUris.parseId(i1)); - mTempProvider.insert(DELETED_TABLE_URI, row1); - - // add expected callbacks to merger - mExpectations.add(new Expectation(Expectation.Type.DELETE, ContentUris.parseId(i1), null)); - - // run merger - SyncResult syncResult = new SyncResult(); - mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); - - // check that all expectations were met - assertEquals("not all expectations were met", 0, mExpectations.size()); - } - - public void testDeleteRowAfterUpdate() { - // add rows to the real provider - Uri i1 = mRealProvider.insert(TABLE_URI, - newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); - - // add a deleted record to the temp provider - ContentValues row1 = newDeletedValues("si1", ACCOUNT, "sv1", ContentUris.parseId(i1)); - mTempProvider.insert(DELETED_TABLE_URI, row1); - - // add expected callbacks to merger - mExpectations.add(new Expectation(Expectation.Type.DELETE, ContentUris.parseId(i1), null)); - - // run merger - SyncResult syncResult = new SyncResult(); - mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); - - // check that all expectations were met - assertEquals("not all expectations were met", 0, mExpectations.size()); - } - - public void testDeleteRowFromServer() { - // add rows to the real provider - Uri i1 = mRealProvider.insert(TABLE_URI, - newValues("d1", "si1", ACCOUNT, "st1", "sv1", null)); - - // add a deleted record to the temp provider - ContentValues row1 = newDeletedValues("si1", ACCOUNT, "sv1", null); - mTempProvider.insert(DELETED_TABLE_URI, row1); - - // add expected callbacks to merger - mExpectations.add(new Expectation(Expectation.Type.DELETE, ContentUris.parseId(i1), null)); - - // run merger - SyncResult syncResult = new SyncResult(); - mMerger.mergeServerDiffs(mSyncContext, ACCOUNT, mTempProvider, syncResult); - - // check that all expectations were met - assertEquals("not all expectations were met", 0, mExpectations.size()); - } - - class MockTableMerger extends AbstractTableMerger { - public MockTableMerger(SQLiteDatabase database, String table, Uri tableURL, - String deletedTable, Uri deletedTableURL) { - super(database, table, tableURL, deletedTable, deletedTableURL); - } - - public void insertRow(ContentProvider diffs, Cursor diffsCursor) { - Expectation expectation = mExpectations.remove(0); - checkExpectation(expectation, - Expectation.Type.INSERT, null /* syncLocalId */, diffsCursor); - } - - public void updateRow(long localPersonID, ContentProvider diffs, Cursor diffsCursor) { - Expectation expectation = mExpectations.remove(0); - checkExpectation(expectation, Expectation.Type.UPDATE, localPersonID, diffsCursor); - } - - public void resolveRow(long localPersonID, String syncID, ContentProvider diffs, - Cursor diffsCursor) { - Expectation expectation = mExpectations.remove(0); - checkExpectation(expectation, Expectation.Type.RESOLVE, localPersonID, diffsCursor); - } - - @Override - public void deleteRow(Cursor cursor) { - Expectation expectation = mExpectations.remove(0); - assertEquals(expectation.mType, Expectation.Type.DELETE); - assertNotNull(expectation.mLocalRowId); - final long localRowId = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); - assertEquals((long)expectation.mLocalRowId, localRowId); - cursor.moveToNext(); - mDb.delete(TABLE_NAME, "_id=" + localRowId, null); - } - - protected void notifyChanges() { - throw new UnsupportedOperationException(); - } - - void checkExpectation(Expectation expectation, - Expectation.Type actualType, Long localRowId, - Cursor cursor) { - assertEquals(expectation.mType, actualType); - assertEquals(expectation.mLocalRowId, localRowId); - - final SortedSet<String> actualKeys = Sets.newSortedSet(cursor.getColumnNames()); - final SortedSet<String> expectedKeys = Sets.newSortedSet(); - for (Map.Entry<String, Object> entry : expectation.mValues.valueSet()) { - expectedKeys.add(entry.getKey()); - } - actualKeys.remove("_id"); - actualKeys.remove("_sync_mark"); - actualKeys.remove("_sync_local_id"); - expectedKeys.remove("_sync_local_id"); - expectedKeys.remove("_id"); - assertEquals("column mismatch", - TextUtils.join(",", expectedKeys), TextUtils.join(",", actualKeys)); - -// if (localRowId != null) { -// assertEquals((long) localRowId, -// cursor.getLong(cursor.getColumnIndexOrThrow("_sync_local_id"))); -// } else { -// assertTrue("unexpected _sync_local_id, " -// + cursor.getLong(cursor.getColumnIndexOrThrow("_sync_local_id")), -// cursor.isNull(cursor.getColumnIndexOrThrow("_sync_local_id"))); -// } - - for (String name : cursor.getColumnNames()) { - if ("_id".equals(name)) { - continue; - } - if (cursor.isNull(cursor.getColumnIndexOrThrow(name))) { - assertNull(expectation.mValues.getAsString(name)); - } else { - String actualValue = - cursor.getString(cursor.getColumnIndexOrThrow(name)); - assertEquals("mismatch on column " + name, - expectation.mValues.getAsString(name), actualValue); - } - } - } - } - - class MockSyncableContentProvider extends SyncableContentProvider { - SQLiteDatabase mDb; - boolean mIsTemporary; - boolean mContainsDiffs; - - private final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); - - private static final int MATCHER_ITEMS = 0; - private static final int MATCHER_DELETED_ITEMS = 1; - - public MockSyncableContentProvider() { - mIsTemporary = false; - setContainsDiffs(false); - sURIMatcher.addURI(CONTENT_URI.getAuthority(), "items", MATCHER_ITEMS); - sURIMatcher.addURI(CONTENT_URI.getAuthority(), "deleted_items", MATCHER_DELETED_ITEMS); - - mDb = SQLiteDatabase.create(null); - mDb.execSQL("CREATE TABLE items (" - + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " - + "data TEXT, " - + "_sync_time TEXT, " - + "_sync_version TEXT, " - + "_sync_id TEXT, " - + "_sync_local_id INTEGER, " - + "_sync_dirty INTEGER NOT NULL DEFAULT 0, " - + "_sync_account TEXT, " - + "_sync_account_type TEXT, " - + "_sync_mark INTEGER)"); - - mDb.execSQL("CREATE TABLE deleted_items (" - + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " - + "_sync_version TEXT, " - + "_sync_id TEXT, " - + "_sync_local_id INTEGER, " - + "_sync_account TEXT, " - + "_sync_account_type TEXT, " - + "_sync_mark INTEGER)"); - } - - public boolean onCreate() { - throw new UnsupportedOperationException(); - } - - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - int match = sURIMatcher.match(uri); - switch (match) { - case MATCHER_ITEMS: - return mDb.query(TABLE_NAME, projection, selection, selectionArgs, - null, null, sortOrder); - case MATCHER_DELETED_ITEMS: - return mDb.query(DELETED_TABLE_NAME, projection, selection, selectionArgs, - null, null, sortOrder); - default: - throw new UnsupportedOperationException("Cannot query URL: " + uri); - } - } - - public String getType(Uri uri) { - throw new UnsupportedOperationException(); - } - - public Uri insert(Uri uri, ContentValues values) { - int match = sURIMatcher.match(uri); - switch (match) { - case MATCHER_ITEMS: { - long id = mDb.insert(TABLE_NAME, "_id", values); - return CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); - } - case MATCHER_DELETED_ITEMS: { - long id = mDb.insert(DELETED_TABLE_NAME, "_id", values); - return CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); - } - default: - throw new UnsupportedOperationException("Cannot query URL: " + uri); - } - } - - public int delete(Uri uri, String selection, String[] selectionArgs) { - int match = sURIMatcher.match(uri); - switch (match) { - case MATCHER_ITEMS: - return mDb.delete(TABLE_NAME, selection, selectionArgs); - case MATCHER_DELETED_ITEMS: - return mDb.delete(DELETED_TABLE_NAME, selection, selectionArgs); - default: - throw new UnsupportedOperationException("Cannot query URL: " + uri); - } - } - - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - int match = sURIMatcher.match(uri); - switch (match) { - case MATCHER_ITEMS: - return mDb.update(TABLE_NAME, values, selection, selectionArgs); - case MATCHER_DELETED_ITEMS: - return mDb.update(DELETED_TABLE_NAME, values, selection, selectionArgs); - default: - throw new UnsupportedOperationException("Cannot query URL: " + uri); - } - } - - protected boolean isTemporary() { - return mIsTemporary; - } - - public void close() { - throw new UnsupportedOperationException(); - } - - protected void bootstrapDatabase(SQLiteDatabase db) { - throw new UnsupportedOperationException(); - } - - protected boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion) { - throw new UnsupportedOperationException(); - } - - protected void onDatabaseOpened(SQLiteDatabase db) { - throw new UnsupportedOperationException(); - } - - public MockSyncableContentProvider getTemporaryInstance() { - MockSyncableContentProvider temp = new MockSyncableContentProvider(); - temp.mIsTemporary = true; - temp.setContainsDiffs(true); - return temp; - } - - public SQLiteDatabase getDatabase() { - return mDb; - } - - public boolean getContainsDiffs() { - return mContainsDiffs; - } - - public void setContainsDiffs(boolean containsDiffs) { - mContainsDiffs = containsDiffs; - } - - protected Iterable<? extends AbstractTableMerger> getMergers() { - throw new UnsupportedOperationException(); - } - - public boolean changeRequiresLocalSync(Uri uri) { - throw new UnsupportedOperationException(); - } - - public void onSyncStart(SyncContext context, Account account) { - throw new UnsupportedOperationException(); - } - - public void onSyncStop(SyncContext context, boolean success) { - throw new UnsupportedOperationException(); - } - - public Account getSyncingAccount() { - throw new UnsupportedOperationException(); - } - - public void merge(SyncContext context, SyncableContentProvider diffs, - TempProviderSyncResult result, SyncResult syncResult) { - throw new UnsupportedOperationException(); - } - - public void onSyncCanceled() { - throw new UnsupportedOperationException(); - } - - public boolean isMergeCancelled() { - return false; - } - - protected int updateInternal(Uri url, ContentValues values, String selection, - String[] selectionArgs) { - throw new UnsupportedOperationException(); - } - - protected int deleteInternal(Uri url, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); - } - - protected Uri insertInternal(Uri url, ContentValues values) { - throw new UnsupportedOperationException(); - } - - protected Cursor queryInternal(Uri url, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - throw new UnsupportedOperationException(); - } - - protected void onAccountsChanged(Account[] accountsArray) { - throw new UnsupportedOperationException(); - } - - protected void deleteRowsForRemovedAccounts(Map<Account, Boolean> accounts, String table - ) { - throw new UnsupportedOperationException(); - } - - public void wipeAccount(Account account) { - throw new UnsupportedOperationException(); - } - - public byte[] readSyncDataBytes(Account account) { - throw new UnsupportedOperationException(); - } - - public void writeSyncDataBytes(Account account, byte[] data) { - throw new UnsupportedOperationException(); - } - } - - class MockSyncContext extends SyncContext { - public MockSyncContext() { - super(null); - } - - @Override - public void setStatusText(String message) { - } - } -} |