moved the hidden sync helpers out of the framework
This commit is contained in:
@ -28137,6 +28137,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"
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
@ -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});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -126,9 +126,6 @@ android.bluetooth.IBluetoothHeadset$Stub
|
||||
android.bluetooth.IBluetoothHeadset$Stub$Proxy
|
||||
android.bluetooth.IBluetoothPbap$Stub
|
||||
android.bluetooth.ScoSocket
|
||||
android.content.AbstractSyncableContentProvider
|
||||
android.content.AbstractSyncableContentProvider$DatabaseHelper
|
||||
android.content.AbstractTableMerger
|
||||
android.content.AsyncQueryHandler
|
||||
android.content.BroadcastReceiver
|
||||
android.content.ComponentName
|
||||
@ -161,15 +158,9 @@ android.content.IntentSender
|
||||
android.content.IntentSender$1
|
||||
android.content.SearchRecentSuggestionsProvider
|
||||
android.content.SearchRecentSuggestionsProvider$DatabaseHelper
|
||||
android.content.SyncAdapter
|
||||
android.content.SyncAdapter$Transport
|
||||
android.content.SyncAdapterType
|
||||
android.content.SyncResult
|
||||
android.content.SyncStateContentProviderHelper
|
||||
android.content.SyncStats
|
||||
android.content.SyncableContentProvider
|
||||
android.content.TempProviderSyncAdapter
|
||||
android.content.TempProviderSyncAdapter$SyncThread
|
||||
android.content.UriMatcher
|
||||
android.content.pm.ActivityInfo
|
||||
android.content.pm.ApplicationInfo
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user