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