• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.content;
18 
19 import android.database.Cursor;
20 import android.database.DatabaseUtils;
21 import android.database.sqlite.SQLiteDatabase;
22 import android.net.Uri;
23 import android.os.Debug;
24 import android.provider.BaseColumns;
25 import static android.provider.SyncConstValue.*;
26 import android.text.TextUtils;
27 import android.util.Log;
28 import android.accounts.Account;
29 
30 /**
31  * @hide
32  */
33 public abstract class AbstractTableMerger
34 {
35     private ContentValues mValues;
36 
37     protected SQLiteDatabase mDb;
38     protected String mTable;
39     protected Uri mTableURL;
40     protected String mDeletedTable;
41     protected Uri mDeletedTableURL;
42     static protected ContentValues mSyncMarkValues;
43     static private boolean TRACE;
44 
45     static {
46         mSyncMarkValues = new ContentValues();
mSyncMarkValues.put(_SYNC_MARK, 1)47         mSyncMarkValues.put(_SYNC_MARK, 1);
48         TRACE = false;
49     }
50 
51     private static final String TAG = "AbstractTableMerger";
52     private static final String[] syncDirtyProjection =
53             new String[] {_SYNC_DIRTY, BaseColumns._ID, _SYNC_ID, _SYNC_VERSION};
54     private static final String[] syncIdAndVersionProjection =
55             new String[] {_SYNC_ID, _SYNC_VERSION};
56 
57     private volatile boolean mIsMergeCancelled;
58 
59     private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and "
60             + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?";
61 
62     private static final String SELECT_BY_SYNC_ID_AND_ACCOUNT =
63             _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?";
64     private static final String SELECT_BY_ID = BaseColumns._ID +"=?";
65 
66     private static final String SELECT_UNSYNCED =
67             "(" + _SYNC_ACCOUNT + " IS NULL OR ("
68                 + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?)) and "
69             + "(" + _SYNC_ID + " IS NULL OR (" + _SYNC_DIRTY + " > 0 and "
70                                               + _SYNC_VERSION + " IS NOT NULL))";
71 
AbstractTableMerger(SQLiteDatabase database, String table, Uri tableURL, String deletedTable, Uri deletedTableURL)72     public AbstractTableMerger(SQLiteDatabase database,
73             String table, Uri tableURL, String deletedTable,
74             Uri deletedTableURL)
75     {
76         mDb = database;
77         mTable = table;
78         mTableURL = tableURL;
79         mDeletedTable = deletedTable;
80         mDeletedTableURL = deletedTableURL;
81         mValues = new ContentValues();
82     }
83 
insertRow(ContentProvider diffs, Cursor diffsCursor)84     public abstract void insertRow(ContentProvider diffs,
85             Cursor diffsCursor);
updateRow(long localPersonID, ContentProvider diffs, Cursor diffsCursor)86     public abstract void updateRow(long localPersonID,
87             ContentProvider diffs, Cursor diffsCursor);
resolveRow(long localPersonID, String syncID, ContentProvider diffs, Cursor diffsCursor)88     public abstract void resolveRow(long localPersonID,
89             String syncID, ContentProvider diffs, Cursor diffsCursor);
90 
91     /**
92      * This is called when it is determined that a row should be deleted from the
93      * ContentProvider. The localCursor is on a table from the local ContentProvider
94      * and its current position is of the row that should be deleted. The localCursor
95      * is only guaranteed to contain the BaseColumns.ID column so the implementation
96      * of deleteRow() must query the database directly if other columns are needed.
97      * <p>
98      * It is the responsibility of the implementation of this method to ensure that the cursor
99      * points to the next row when this method returns, either by calling Cursor.deleteRow() or
100      * Cursor.next().
101      *
102      * @param localCursor The Cursor into the local table, which points to the row that
103      *   is to be deleted.
104      */
deleteRow(Cursor localCursor)105     public void deleteRow(Cursor localCursor) {
106         localCursor.deleteRow();
107     }
108 
109     /**
110      * After {@link #merge} has completed, this method is called to send
111      * notifications to {@link android.database.ContentObserver}s of changes
112      * to the containing {@link ContentProvider}.  These notifications likely
113      * do not want to request a sync back to the network.
114      */
notifyChanges()115     protected abstract void notifyChanges();
116 
findInCursor(Cursor cursor, int column, String id)117     private static boolean findInCursor(Cursor cursor, int column, String id) {
118         while (!cursor.isAfterLast() && !cursor.isNull(column)) {
119             int comp = id.compareTo(cursor.getString(column));
120             if (comp > 0) {
121                 cursor.moveToNext();
122                 continue;
123             }
124             return comp == 0;
125         }
126         return false;
127     }
128 
onMergeCancelled()129     public void onMergeCancelled() {
130         mIsMergeCancelled = true;
131     }
132 
133     /**
134      * Carry out a merge of the given diffs, and add the results to
135      * the given MergeResult.  If we are the first merge to find
136      * client-side diffs, we'll use the given ContentProvider to
137      * construct a temporary instance to hold them.
138      */
merge(final SyncContext context, final Account account, final SyncableContentProvider serverDiffs, TempProviderSyncResult result, SyncResult syncResult, SyncableContentProvider temporaryInstanceFactory)139     public void merge(final SyncContext context,
140             final Account account,
141             final SyncableContentProvider serverDiffs,
142             TempProviderSyncResult result,
143             SyncResult syncResult, SyncableContentProvider temporaryInstanceFactory) {
144         mIsMergeCancelled = false;
145         if (serverDiffs != null) {
146             if (!mDb.isDbLockedByCurrentThread()) {
147                 throw new IllegalStateException("this must be called from within a DB transaction");
148             }
149             mergeServerDiffs(context, account, serverDiffs, syncResult);
150             notifyChanges();
151         }
152 
153         if (result != null) {
154             findLocalChanges(result, temporaryInstanceFactory, account, syncResult);
155         }
156         if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "merge complete");
157     }
158 
159     /**
160      * @hide this is public for testing purposes only
161      */
mergeServerDiffs(SyncContext context, Account account, SyncableContentProvider serverDiffs, SyncResult syncResult)162     public void mergeServerDiffs(SyncContext context,
163             Account account, SyncableContentProvider serverDiffs, SyncResult syncResult) {
164         boolean diffsArePartial = serverDiffs.getContainsDiffs();
165         // mark the current rows so that we can distinguish these from new
166         // inserts that occur during the merge
167         mDb.update(mTable, mSyncMarkValues, null, null);
168         if (mDeletedTable != null) {
169             mDb.update(mDeletedTable, mSyncMarkValues, null, null);
170         }
171 
172         Cursor localCursor = null;
173         Cursor deletedCursor = null;
174         Cursor diffsCursor = null;
175         try {
176             // load the local database entries, so we can merge them with the server
177             final String[] accountSelectionArgs = new String[]{account.name, account.type};
178             localCursor = mDb.query(mTable, syncDirtyProjection,
179                     SELECT_MARKED, accountSelectionArgs, null, null,
180                     mTable + "." + _SYNC_ID);
181             if (mDeletedTable != null) {
182                 deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection,
183                         SELECT_MARKED, accountSelectionArgs, null, null,
184                         mDeletedTable + "." + _SYNC_ID);
185             } else {
186                 deletedCursor =
187                         mDb.rawQuery("select 'a' as _sync_id, 'b' as _sync_version limit 0", null);
188             }
189 
190             // Apply updates and insertions from the server
191             diffsCursor = serverDiffs.query(mTableURL,
192                     null, null, null, mTable + "." + _SYNC_ID);
193             int deletedSyncIDColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_ID);
194             int deletedSyncVersionColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_VERSION);
195             int serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID);
196             int serverSyncVersionColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_VERSION);
197             int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID);
198 
199             String lastSyncId = null;
200             int diffsCount = 0;
201             int localCount = 0;
202             localCursor.moveToFirst();
203             deletedCursor.moveToFirst();
204             while (diffsCursor.moveToNext()) {
205                 if (mIsMergeCancelled) {
206                     return;
207                 }
208                 mDb.yieldIfContended();
209                 String serverSyncId = diffsCursor.getString(serverSyncIDColumn);
210                 String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn);
211                 long localRowId = 0;
212                 String localSyncVersion = null;
213 
214                 diffsCount++;
215                 context.setStatusText("Processing " + diffsCount + "/"
216                         + diffsCursor.getCount());
217                 if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processing server entry " +
218                         diffsCount + ", " + serverSyncId);
219 
220                 if (TRACE) {
221                     if (diffsCount == 10) {
222                         Debug.startMethodTracing("atmtrace");
223                     }
224                     if (diffsCount == 20) {
225                         Debug.stopMethodTracing();
226                     }
227                 }
228 
229                 boolean conflict = false;
230                 boolean update = false;
231                 boolean insert = false;
232 
233                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
234                     Log.v(TAG, "found event with serverSyncID " + serverSyncId);
235                 }
236                 if (TextUtils.isEmpty(serverSyncId)) {
237                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
238                         Log.e(TAG, "server entry doesn't have a serverSyncID");
239                     }
240                     continue;
241                 }
242 
243                 // It is possible that the sync adapter wrote the same record multiple times,
244                 // e.g. if the same record came via multiple feeds. If this happens just ignore
245                 // the duplicate records.
246                 if (serverSyncId.equals(lastSyncId)) {
247                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
248                         Log.v(TAG, "skipping record with duplicate remote server id " + lastSyncId);
249                     }
250                     continue;
251                 }
252                 lastSyncId = serverSyncId;
253 
254                 String localSyncID = null;
255                 boolean localSyncDirty = false;
256 
257                 while (!localCursor.isAfterLast()) {
258                     if (mIsMergeCancelled) {
259                         return;
260                     }
261                     localCount++;
262                     localSyncID = localCursor.getString(2);
263 
264                     // If the local record doesn't have a _sync_id then
265                     // it is new. Ignore it for now, we will send an insert
266                     // the the server later.
267                     if (TextUtils.isEmpty(localSyncID)) {
268                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
269                             Log.v(TAG, "local record " +
270                                     localCursor.getLong(1) +
271                                     " has no _sync_id, ignoring");
272                         }
273                         localCursor.moveToNext();
274                         localSyncID = null;
275                         continue;
276                     }
277 
278                     int comp = serverSyncId.compareTo(localSyncID);
279 
280                     // the local DB has a record that the server doesn't have
281                     if (comp > 0) {
282                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
283                             Log.v(TAG, "local record " +
284                                     localCursor.getLong(1) +
285                                     " has _sync_id " + localSyncID +
286                                     " that is < server _sync_id " + serverSyncId);
287                         }
288                         if (diffsArePartial) {
289                             localCursor.moveToNext();
290                         } else {
291                             deleteRow(localCursor);
292                             if (mDeletedTable != null) {
293                                 mDb.delete(mDeletedTable, _SYNC_ID +"=?", new String[] {localSyncID});
294                             }
295                             syncResult.stats.numDeletes++;
296                             mDb.yieldIfContended();
297                         }
298                         localSyncID = null;
299                         continue;
300                     }
301 
302                     // the server has a record that the local DB doesn't have
303                     if (comp < 0) {
304                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
305                             Log.v(TAG, "local record " +
306                                     localCursor.getLong(1) +
307                                     " has _sync_id " + localSyncID +
308                                     " that is > server _sync_id " + serverSyncId);
309                         }
310                         localSyncID = null;
311                     }
312 
313                     // the server and the local DB both have this record
314                     if (comp == 0) {
315                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
316                             Log.v(TAG, "local record " +
317                                     localCursor.getLong(1) +
318                                     " has _sync_id " + localSyncID +
319                                     " that matches the server _sync_id");
320                         }
321                         localSyncDirty = localCursor.getInt(0) != 0;
322                         localRowId = localCursor.getLong(1);
323                         localSyncVersion = localCursor.getString(3);
324                         localCursor.moveToNext();
325                     }
326 
327                     break;
328                 }
329 
330                 // If this record is in the deleted table then update the server version
331                 // in the deleted table, if necessary, and then ignore it here.
332                 // We will send a deletion indication to the server down a
333                 // little further.
334                 if (findInCursor(deletedCursor, deletedSyncIDColumn, serverSyncId)) {
335                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
336                         Log.v(TAG, "remote record " + serverSyncId + " is in the deleted table");
337                     }
338                     final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn);
339                     if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) {
340                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
341                             Log.v(TAG, "setting version of deleted record " + serverSyncId + " to "
342                                     + serverSyncVersion);
343                         }
344                         ContentValues values = new ContentValues();
345                         values.put(_SYNC_VERSION, serverSyncVersion);
346                         mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId});
347                     }
348                     continue;
349                 }
350 
351                 // If the _sync_local_id is present in the diffsCursor
352                 // then this record corresponds to a local record that was just
353                 // inserted into the server and the _sync_local_id is the row id
354                 // of the local record. Set these fields so that the next check
355                 // treats this record as an update, which will allow the
356                 // merger to update the record with the server's sync id
357                 if (!diffsCursor.isNull(serverSyncLocalIdColumn)) {
358                     localRowId = diffsCursor.getLong(serverSyncLocalIdColumn);
359                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
360                         Log.v(TAG, "the remote record with sync id " + serverSyncId
361                                 + " has a local sync id, " + localRowId);
362                     }
363                     localSyncID = serverSyncId;
364                     localSyncDirty = false;
365                     localSyncVersion = null;
366                 }
367 
368                 if (!TextUtils.isEmpty(localSyncID)) {
369                     // An existing server item has changed
370                     // If serverSyncVersion is null, there is no edit URL;
371                     // server won't let this change be written.
372                     boolean recordChanged = (localSyncVersion == null) ||
373                             (serverSyncVersion == null) ||
374                             !serverSyncVersion.equals(localSyncVersion);
375                     if (recordChanged) {
376                         if (localSyncDirty) {
377                             if (Log.isLoggable(TAG, Log.VERBOSE)) {
378                                 Log.v(TAG, "remote record " + serverSyncId
379                                         + " conflicts with local _sync_id " + localSyncID
380                                         + ", local _id " + localRowId);
381                             }
382                             conflict = true;
383                         } else {
384                             if (Log.isLoggable(TAG, Log.VERBOSE)) {
385                                 Log.v(TAG,
386                                         "remote record " +
387                                                 serverSyncId +
388                                                 " updates local _sync_id " +
389                                                 localSyncID + ", local _id " +
390                                                 localRowId);
391                             }
392                             update = true;
393                         }
394                     } else {
395                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
396                             Log.v(TAG,
397                                     "Skipping update: localSyncVersion: " + localSyncVersion +
398                                     ", serverSyncVersion: " + serverSyncVersion);
399                         }
400                     }
401                 } else {
402                     // the local db doesn't know about this record so add it
403                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
404                         Log.v(TAG, "remote record " + serverSyncId + " is new, inserting");
405                     }
406                     insert = true;
407                 }
408 
409                 if (update) {
410                     updateRow(localRowId, serverDiffs, diffsCursor);
411                     syncResult.stats.numUpdates++;
412                 } else if (conflict) {
413                     resolveRow(localRowId, serverSyncId, serverDiffs, diffsCursor);
414                     syncResult.stats.numUpdates++;
415                 } else if (insert) {
416                     insertRow(serverDiffs, diffsCursor);
417                     syncResult.stats.numInserts++;
418                 }
419             }
420 
421             if (Log.isLoggable(TAG, Log.VERBOSE)) {
422                 Log.v(TAG, "processed " + diffsCount + " server entries");
423             }
424 
425             // If tombstones aren't in use delete any remaining local rows that
426             // don't have corresponding server rows. Keep the rows that don't
427             // have a sync id since those were created locally and haven't been
428             // synced to the server yet.
429             if (!diffsArePartial) {
430                 while (!localCursor.isAfterLast() && !TextUtils.isEmpty(localCursor.getString(2))) {
431                     if (mIsMergeCancelled) {
432                         return;
433                     }
434                     localCount++;
435                     final String localSyncId = localCursor.getString(2);
436                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
437                         Log.v(TAG,
438                                 "deleting local record " +
439                                         localCursor.getLong(1) +
440                                         " _sync_id " + localSyncId);
441                     }
442                     deleteRow(localCursor);
443                     if (mDeletedTable != null) {
444                         mDb.delete(mDeletedTable, _SYNC_ID + "=?", new String[] {localSyncId});
445                     }
446                     syncResult.stats.numDeletes++;
447                     mDb.yieldIfContended();
448                 }
449             }
450             if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "checked " + localCount +
451                     " local entries");
452         } finally {
453             if (diffsCursor != null) diffsCursor.close();
454             if (localCursor != null) localCursor.close();
455             if (deletedCursor != null) deletedCursor.close();
456         }
457 
458 
459         if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "applying deletions from the server");
460 
461         // Apply deletions from the server
462         if (mDeletedTableURL != null) {
463             diffsCursor = serverDiffs.query(mDeletedTableURL, null, null, null, null);
464             try {
465                 while (diffsCursor.moveToNext()) {
466                     if (mIsMergeCancelled) {
467                         return;
468                     }
469                     // delete all rows that match each element in the diffsCursor
470                     fullyDeleteMatchingRows(diffsCursor, account, syncResult);
471                     mDb.yieldIfContended();
472                 }
473             } finally {
474                 diffsCursor.close();
475             }
476         }
477     }
478 
fullyDeleteMatchingRows(Cursor diffsCursor, Account account, SyncResult syncResult)479     private void fullyDeleteMatchingRows(Cursor diffsCursor, Account account,
480             SyncResult syncResult) {
481         int serverSyncIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID);
482         final boolean deleteBySyncId = !diffsCursor.isNull(serverSyncIdColumn);
483 
484         // delete the rows explicitly so that the delete operation can be overridden
485         final String[] selectionArgs;
486         Cursor c = null;
487         try {
488             if (deleteBySyncId) {
489                 selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn),
490                         account.name, account.type};
491                 c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_SYNC_ID_AND_ACCOUNT,
492                         selectionArgs, null, null, null);
493             } else {
494                 int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID);
495                 selectionArgs = new String[]{diffsCursor.getString(serverSyncLocalIdColumn)};
496                 c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_ID, selectionArgs,
497                         null, null, null);
498             }
499             c.moveToFirst();
500             while (!c.isAfterLast()) {
501                 deleteRow(c); // advances the cursor
502                 syncResult.stats.numDeletes++;
503             }
504         } finally {
505           if (c != null) c.close();
506         }
507         if (deleteBySyncId && mDeletedTable != null) {
508             mDb.delete(mDeletedTable, SELECT_BY_SYNC_ID_AND_ACCOUNT, selectionArgs);
509         }
510     }
511 
512     /**
513      * Converts cursor into a Map, using the correct types for the values.
514      */
cursorRowToContentValues(Cursor cursor, ContentValues map)515     protected void cursorRowToContentValues(Cursor cursor, ContentValues map) {
516         DatabaseUtils.cursorRowToContentValues(cursor, map);
517     }
518 
519     /**
520      * Finds local changes, placing the results in the given result object.
521      * @param temporaryInstanceFactory As an optimization for the case
522      * where there are no client-side diffs, mergeResult may initially
523      * have no {@link TempProviderSyncResult#tempContentProvider}.  If this is
524      * the first in the sequence of AbstractTableMergers to find
525      * client-side diffs, it will use the given ContentProvider to
526      * create a temporary instance and store its {@link
527      * android.content.ContentProvider} in the mergeResult.
528      * @param account
529      * @param syncResult
530      */
findLocalChanges(TempProviderSyncResult mergeResult, SyncableContentProvider temporaryInstanceFactory, Account account, SyncResult syncResult)531     private void findLocalChanges(TempProviderSyncResult mergeResult,
532             SyncableContentProvider temporaryInstanceFactory, Account account,
533             SyncResult syncResult) {
534         SyncableContentProvider clientDiffs = mergeResult.tempContentProvider;
535         if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client updates");
536 
537         final String[] accountSelectionArgs = new String[]{account.name, account.type};
538 
539         // Generate the client updates and insertions
540         // Create a cursor for dirty records
541         long numInsertsOrUpdates = 0;
542         Cursor localChangesCursor = mDb.query(mTable, null, SELECT_UNSYNCED, accountSelectionArgs,
543                 null, null, null);
544         try {
545             numInsertsOrUpdates = localChangesCursor.getCount();
546             while (localChangesCursor.moveToNext()) {
547                 if (mIsMergeCancelled) {
548                     return;
549                 }
550                 if (clientDiffs == null) {
551                     clientDiffs = temporaryInstanceFactory.getTemporaryInstance();
552                 }
553                 mValues.clear();
554                 cursorRowToContentValues(localChangesCursor, mValues);
555                 mValues.remove("_id");
556                 DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues,
557                         _SYNC_LOCAL_ID);
558                 clientDiffs.insert(mTableURL, mValues);
559             }
560         } finally {
561           localChangesCursor.close();
562         }
563 
564         // Generate the client deletions
565         if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client deletions");
566         long numEntries = DatabaseUtils.queryNumEntries(mDb, mTable);
567         long numDeletedEntries = 0;
568         if (mDeletedTable != null) {
569             Cursor deletedCursor = mDb.query(mDeletedTable,
570                     syncIdAndVersionProjection,
571                     _SYNC_ACCOUNT + "=? AND " + _SYNC_ACCOUNT_TYPE + "=? AND "
572                             + _SYNC_ID + " IS NOT NULL", accountSelectionArgs,
573                     null, null, mDeletedTable + "." + _SYNC_ID);
574             try {
575                 numDeletedEntries = deletedCursor.getCount();
576                 while (deletedCursor.moveToNext()) {
577                     if (mIsMergeCancelled) {
578                         return;
579                     }
580                     if (clientDiffs == null) {
581                         clientDiffs = temporaryInstanceFactory.getTemporaryInstance();
582                     }
583                     mValues.clear();
584                     DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues);
585                     clientDiffs.insert(mDeletedTableURL, mValues);
586                 }
587             } finally {
588                 deletedCursor.close();
589             }
590         }
591 
592         if (clientDiffs != null) {
593             mergeResult.tempContentProvider = clientDiffs;
594         }
595         syncResult.stats.numDeletes += numDeletedEntries;
596         syncResult.stats.numUpdates += numInsertsOrUpdates;
597         syncResult.stats.numEntries += numEntries;
598     }
599 }
600