AbstractTableMergerpublic abstract class AbstractTableMerger extends Object
Fields Summary |
---|
private ContentValues | mValues | protected android.database.sqlite.SQLiteDatabase | mDb | protected String | mTable | protected android.net.Uri | mTableURL | protected String | mDeletedTable | protected android.net.Uri | mDeletedTableURL | protected static ContentValues | mSyncMarkValues | private static boolean | TRACE | private static final String | TAG | private static final String[] | syncDirtyProjection | private static final String[] | syncIdAndVersionProjection | private volatile boolean | mIsMergeCancelled | private static final String | SELECT_MARKED | private static final String | SELECT_BY_SYNC_ID_AND_ACCOUNT | private static final String | SELECT_BY_ID | private static final String | SELECT_UNSYNCED |
Methods Summary |
---|
protected void | cursorRowToContentValues(android.database.Cursor cursor, ContentValues map)Converts cursor into a Map, using the correct types for the values.
DatabaseUtils.cursorRowToContentValues(cursor, map);
| public void | deleteRow(android.database.Cursor localCursor)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.
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().
localCursor.deleteRow();
| private static boolean | findInCursor(android.database.Cursor cursor, int column, java.lang.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;
| private void | findLocalChanges(TempProviderSyncResult mergeResult, SyncableContentProvider temporaryInstanceFactory, java.lang.String account, SyncResult syncResult)Finds local changes, placing the results in the given result object.
SyncableContentProvider clientDiffs = mergeResult.tempContentProvider;
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client updates");
final String[] accountSelectionArgs = new String[]{account};
// Generate the client updates and insertions
// Create a cursor for dirty records
Cursor localChangesCursor = mDb.query(mTable, null, SELECT_UNSYNCED, accountSelectionArgs,
null, null, null);
long numInsertsOrUpdates = localChangesCursor.getCount();
while (localChangesCursor.moveToNext()) {
if (mIsMergeCancelled) {
localChangesCursor.close();
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);
}
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_ID + " IS NOT NULL", accountSelectionArgs,
null, null, mDeletedTable + "." + _SYNC_ID);
numDeletedEntries = deletedCursor.getCount();
while (deletedCursor.moveToNext()) {
if (mIsMergeCancelled) {
deletedCursor.close();
return;
}
if (clientDiffs == null) {
clientDiffs = temporaryInstanceFactory.getTemporaryInstance();
}
mValues.clear();
DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues);
clientDiffs.insert(mDeletedTableURL, mValues);
}
deletedCursor.close();
}
if (clientDiffs != null) {
mergeResult.tempContentProvider = clientDiffs;
}
syncResult.stats.numDeletes += numDeletedEntries;
syncResult.stats.numUpdates += numInsertsOrUpdates;
syncResult.stats.numEntries += numEntries;
| private void | fullyDeleteMatchingRows(android.database.Cursor diffsCursor, java.lang.String 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 Cursor c;
final String[] selectionArgs;
if (deleteBySyncId) {
selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn), account};
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);
}
try {
c.moveToFirst();
while (!c.isAfterLast()) {
deleteRow(c); // advances the cursor
syncResult.stats.numDeletes++;
}
} finally {
c.deactivate();
}
if (deleteBySyncId && mDeletedTable != null) {
mDb.delete(mDeletedTable, SELECT_BY_SYNC_ID_AND_ACCOUNT, selectionArgs);
}
| public abstract void | insertRow(ContentProvider diffs, android.database.Cursor diffsCursor)
| public void | merge(SyncContext context, java.lang.String account, SyncableContentProvider serverDiffs, TempProviderSyncResult result, SyncResult syncResult, SyncableContentProvider temporaryInstanceFactory)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.
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");
| public void | mergeServerDiffs(SyncContext context, java.lang.String 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);
}
// load the local database entries, so we can merge them with the server
final String[] accountSelectionArgs = new String[]{account};
Cursor localCursor = mDb.query(mTable, syncDirtyProjection,
SELECT_MARKED, accountSelectionArgs, null, null,
mTable + "." + _SYNC_ID);
Cursor deletedCursor;
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
Cursor 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) {
localCursor.close();
deletedCursor.close();
diffsCursor.close();
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) {
localCursor.deactivate();
deletedCursor.deactivate();
diffsCursor.deactivate();
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
boolean recordChanged = (localSyncVersion == 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 {
// 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) {
localCursor.deactivate();
deletedCursor.deactivate();
diffsCursor.deactivate();
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");
diffsCursor.deactivate();
localCursor.deactivate();
deletedCursor.deactivate();
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);
while (diffsCursor.moveToNext()) {
if (mIsMergeCancelled) {
diffsCursor.deactivate();
return;
}
// delete all rows that match each element in the diffsCursor
fullyDeleteMatchingRows(diffsCursor, account, syncResult);
mDb.yieldIfContended();
}
diffsCursor.deactivate();
}
| protected abstract void | notifyChanges()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.
| public void | onMergeCancelled()
mIsMergeCancelled = true;
| public abstract void | resolveRow(long localPersonID, java.lang.String syncID, ContentProvider diffs, android.database.Cursor diffsCursor)
| public abstract void | updateRow(long localPersonID, ContentProvider diffs, android.database.Cursor diffsCursor)
|
|