FileDocCategorySizeDatePackage
AbstractTableMerger.javaAPI DocAndroid 1.5 API24847Wed May 06 22:41:54 BST 2009android.content

AbstractTableMerger

public abstract class AbstractTableMerger extends Object
hide

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
Constructors Summary
public AbstractTableMerger(android.database.sqlite.SQLiteDatabase database, String table, android.net.Uri tableURL, String deletedTable, android.net.Uri deletedTableURL)


      
                 
             
    
        mDb = database;
        mTable = table;
        mTableURL = tableURL;
        mDeletedTable = deletedTable;
        mDeletedTableURL = deletedTableURL;
        mValues = new ContentValues();
    
Methods Summary
protected voidcursorRowToContentValues(android.database.Cursor cursor, ContentValues map)
Converts cursor into a Map, using the correct types for the values.

        DatabaseUtils.cursorRowToContentValues(cursor, map);
    
public voiddeleteRow(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().

param
localCursor The Cursor into the local table, which points to the row that is to be deleted.

        localCursor.deleteRow();
    
private static booleanfindInCursor(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 voidfindLocalChanges(TempProviderSyncResult mergeResult, SyncableContentProvider temporaryInstanceFactory, java.lang.String account, SyncResult syncResult)
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 android.content.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 ContentProvider} in the mergeResult.
param
account
param
syncResult

        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 voidfullyDeleteMatchingRows(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 voidinsertRow(ContentProvider diffs, android.database.Cursor diffsCursor)

public voidmerge(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 voidmergeServerDiffs(SyncContext context, java.lang.String account, SyncableContentProvider serverDiffs, SyncResult syncResult)

hide
this is public for testing purposes only

        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 voidnotifyChanges()
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 voidonMergeCancelled()

        mIsMergeCancelled = true;
    
public abstract voidresolveRow(long localPersonID, java.lang.String syncID, ContentProvider diffs, android.database.Cursor diffsCursor)

public abstract voidupdateRow(long localPersonID, ContentProvider diffs, android.database.Cursor diffsCursor)