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 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; 28import android.accounts.Account; 29 30/** 31 * @hide 32 */ 33public 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(); 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 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 84 public abstract void insertRow(ContentProvider diffs, 85 Cursor diffsCursor); 86 public abstract void updateRow(long localPersonID, 87 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 */ 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 */ 115 protected abstract void notifyChanges(); 116 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 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 */ 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 */ 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 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 */ 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 */ 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