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