1/*
2 * Copyright (C) 2010 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 */
16package com.android.providers.calendar;
17
18
19import com.android.common.content.SyncStateContentProviderHelper;
20
21import android.database.Cursor;
22import android.database.DatabaseUtils;
23import android.database.sqlite.SQLiteDatabase;
24import android.test.mock.MockContext;
25import android.test.suitebuilder.annotation.MediumTest;
26import android.text.TextUtils;
27import android.util.Log;
28
29import java.util.Arrays;
30
31import junit.framework.TestCase;
32
33public class CalendarDatabaseHelperTest extends TestCase {
34    private static final String TAG = "CDbHelperTest";
35
36    private SQLiteDatabase mBadDb;
37    private SQLiteDatabase mGoodDb;
38    private DatabaseUtils.InsertHelper mBadEventsInserter;
39    private DatabaseUtils.InsertHelper mGoodEventsInserter;
40
41    @Override
42    public void setUp() {
43        mBadDb = SQLiteDatabase.create(null);
44        assertNotNull(mBadDb);
45        mGoodDb = SQLiteDatabase.create(null);
46        assertNotNull(mGoodDb);
47    }
48
49    protected void bootstrapDbVersion50(SQLiteDatabase db) {
50
51        // TODO remove the dependency on this system class
52        SyncStateContentProviderHelper syncStateHelper = new SyncStateContentProviderHelper();
53        syncStateHelper.createDatabase(db);
54
55        db.execSQL("CREATE TABLE Calendars (" +
56                        "_id INTEGER PRIMARY KEY," +
57                        "_sync_account TEXT," +
58                        "_sync_id TEXT," +
59                        "_sync_version TEXT," +
60                        "_sync_time TEXT," +            // UTC
61                        "_sync_local_id INTEGER," +
62                        "_sync_dirty INTEGER," +
63                        "_sync_mark INTEGER," + // Used to filter out new rows
64                        "url TEXT," +
65                        "name TEXT," +
66                        "displayName TEXT," +
67                        "hidden INTEGER NOT NULL DEFAULT 0," +
68                        "color INTEGER," +
69                        "access_level INTEGER," +
70                        "selected INTEGER NOT NULL DEFAULT 1," +
71                        "sync_events INTEGER NOT NULL DEFAULT 0," +
72                        "location TEXT," +
73                        "timezone TEXT" +
74                        ");");
75
76        // Trigger to remove a calendar's events when we delete the calendar
77        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
78                    "BEGIN " +
79                        "DELETE FROM Events WHERE calendar_id = old._id;" +
80                        "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
81                    "END");
82
83        // TODO: do we need both dtend and duration?
84        db.execSQL("CREATE TABLE Events (" +
85                        "_id INTEGER PRIMARY KEY," +
86                        "_sync_account TEXT," +
87                        "_sync_id TEXT," +
88                        "_sync_version TEXT," +
89                        "_sync_time TEXT," +            // UTC
90                        "_sync_local_id INTEGER," +
91                        "_sync_dirty INTEGER," +
92                        "_sync_mark INTEGER," + // To filter out new rows
93                        // TODO remove NOT NULL when upgrade rebuilds events to have
94                        // true v50 schema
95                        "calendar_id INTEGER NOT NULL," +
96                        "htmlUri TEXT," +
97                        "title TEXT," +
98                        "eventLocation TEXT," +
99                        "description TEXT," +
100                        "eventStatus INTEGER," +
101                        "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
102                        "commentsUri TEXT," +
103                        "dtstart INTEGER," +               // millis since epoch
104                        "dtend INTEGER," +                 // millis since epoch
105                        "eventTimezone TEXT," +         // timezone for event
106                        "duration TEXT," +
107                        "allDay INTEGER NOT NULL DEFAULT 0," +
108                        "visibility INTEGER NOT NULL DEFAULT 0," +
109                        "transparency INTEGER NOT NULL DEFAULT 0," +
110                        "hasAlarm INTEGER NOT NULL DEFAULT 0," +
111                        "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
112                        "rrule TEXT," +
113                        "rdate TEXT," +
114                        "exrule TEXT," +
115                        "exdate TEXT," +
116                        "originalEvent TEXT," +
117                        "originalInstanceTime INTEGER," +  // millis since epoch
118                        "lastDate INTEGER" +               // millis since epoch
119                    ");");
120
121        db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (calendar_id);");
122
123        db.execSQL("CREATE TABLE EventsRawTimes (" +
124                        "_id INTEGER PRIMARY KEY," +
125                        "event_id INTEGER NOT NULL," +
126                        "dtstart2445 TEXT," +
127                        "dtend2445 TEXT," +
128                        "originalInstanceTime2445 TEXT," +
129                        "lastDate2445 TEXT," +
130                        "UNIQUE (event_id)" +
131                    ");");
132
133        // NOTE: we do not create a trigger to delete an event's instances upon update,
134        // as all rows currently get updated during a merge.
135
136        db.execSQL("CREATE TABLE DeletedEvents (" +
137                        "_sync_id TEXT," +
138                        "_sync_version TEXT," +
139                        "_sync_account TEXT," +
140                        "_sync_mark INTEGER" + // To filter out new rows
141                    ");");
142
143        db.execSQL("CREATE TABLE Instances (" +
144                        "_id INTEGER PRIMARY KEY," +
145                        "event_id INTEGER," +
146                        "begin INTEGER," +         // UTC millis
147                        "end INTEGER," +           // UTC millis
148                        "startDay INTEGER," +      // Julian start day
149                        "endDay INTEGER," +        // Julian end day
150                        "startMinute INTEGER," +   // minutes from midnight
151                        "endMinute INTEGER," +     // minutes from midnight
152                        "UNIQUE (event_id, begin, end)" +
153                    ");");
154
155        db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (startDay);");
156
157        db.execSQL("CREATE TABLE CalendarMetaData (" +
158                        "_id INTEGER PRIMARY KEY," +
159                        "localTimezone TEXT," +
160                        "minInstance INTEGER," +      // UTC millis
161                        "maxInstance INTEGER," +      // UTC millis
162                        "minBusyBits INTEGER," +      // UTC millis
163                        "maxBusyBits INTEGER" +       // UTC millis
164        ");");
165
166        db.execSQL("CREATE TABLE BusyBits(" +
167                        "day INTEGER PRIMARY KEY," +  // the Julian day
168                        "busyBits INTEGER," +         // 24 bits for 60-minute intervals
169                        "allDayCount INTEGER" +       // number of all-day events
170        ");");
171
172        db.execSQL("CREATE TABLE Attendees (" +
173                        "_id INTEGER PRIMARY KEY," +
174                        "event_id INTEGER," +
175                        "attendeeName TEXT," +
176                        "attendeeEmail TEXT," +
177                        "attendeeStatus INTEGER," +
178                        "attendeeRelationship INTEGER," +
179                        "attendeeType INTEGER" +
180                   ");");
181
182        db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (event_id);");
183
184        db.execSQL("CREATE TABLE Reminders (" +
185                        "_id INTEGER PRIMARY KEY," +
186                        "event_id INTEGER," +
187                        "minutes INTEGER," +
188                        "method INTEGER NOT NULL" +
189                        " DEFAULT 0);");
190
191        db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (event_id);");
192
193        // This table stores the Calendar notifications that have gone off.
194        db.execSQL("CREATE TABLE CalendarAlerts (" +
195                        "_id INTEGER PRIMARY KEY," +
196                        "event_id INTEGER," +
197                        "begin INTEGER NOT NULL," +        // UTC millis
198                        "end INTEGER NOT NULL," +          // UTC millis
199                        "alarmTime INTEGER NOT NULL," +    // UTC millis
200                        "state INTEGER NOT NULL," +
201                        "minutes INTEGER," +
202                        "UNIQUE (alarmTime, begin, event_id)" +
203                   ");");
204
205        db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (event_id);");
206
207        db.execSQL("CREATE TABLE ExtendedProperties (" +
208                        "_id INTEGER PRIMARY KEY," +
209                        "event_id INTEGER," +
210                        "name TEXT," +
211                        "value TEXT" +
212                   ");");
213
214        db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (event_id);");
215
216        // Trigger to remove data tied to an event when we delete that event.
217        db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
218                    "BEGIN " +
219                        "DELETE FROM Instances WHERE event_id = old._id;" +
220                        "DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
221                        "DELETE FROM Attendees WHERE event_id = old._id;" +
222                        "DELETE FROM Reminders WHERE event_id = old._id;" +
223                        "DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
224                        "DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
225                    "END");
226
227        // Triggers to set the _sync_dirty flag when an attendee is changed,
228        // inserted or deleted
229        db.execSQL("CREATE TRIGGER attendees_update UPDATE ON Attendees " +
230                    "BEGIN " +
231                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
232                    "END");
233        db.execSQL("CREATE TRIGGER attendees_insert INSERT ON Attendees " +
234                    "BEGIN " +
235                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
236                    "END");
237        db.execSQL("CREATE TRIGGER attendees_delete DELETE ON Attendees " +
238                    "BEGIN " +
239                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
240                    "END");
241
242        // Triggers to set the _sync_dirty flag when a reminder is changed,
243        // inserted or deleted
244        db.execSQL("CREATE TRIGGER reminders_update UPDATE ON Reminders " +
245                    "BEGIN " +
246                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
247                    "END");
248        db.execSQL("CREATE TRIGGER reminders_insert INSERT ON Reminders " +
249                    "BEGIN " +
250                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
251                    "END");
252        db.execSQL("CREATE TRIGGER reminders_delete DELETE ON Reminders " +
253                    "BEGIN " +
254                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
255                    "END");
256        // Triggers to set the _sync_dirty flag when an extended property is changed,
257        // inserted or deleted
258        db.execSQL("CREATE TRIGGER extended_properties_update UPDATE ON ExtendedProperties " +
259                    "BEGIN " +
260                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
261                    "END");
262        db.execSQL("CREATE TRIGGER extended_properties_insert UPDATE ON ExtendedProperties " +
263                    "BEGIN " +
264                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
265                    "END");
266        db.execSQL("CREATE TRIGGER extended_properties_delete UPDATE ON ExtendedProperties " +
267                    "BEGIN " +
268                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
269                    "END");
270    }
271
272    private void createVersion67EventsTable(SQLiteDatabase db) {
273        db.execSQL("CREATE TABLE Events (" +
274                "_id INTEGER PRIMARY KEY," +
275                "_sync_account TEXT," +
276                "_sync_account_type TEXT," +
277                "_sync_id TEXT," +
278                "_sync_version TEXT," +
279                "_sync_time TEXT," +            // UTC
280                "_sync_local_id INTEGER," +
281                "_sync_dirty INTEGER," +
282                "_sync_mark INTEGER," + // To filter out new rows
283                "calendar_id INTEGER NOT NULL," +
284                "htmlUri TEXT," +
285                "title TEXT," +
286                "eventLocation TEXT," +
287                "description TEXT," +
288                "eventStatus INTEGER," +
289                "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
290                "commentsUri TEXT," +
291                "dtstart INTEGER," +               // millis since epoch
292                "dtend INTEGER," +                 // millis since epoch
293                "eventTimezone TEXT," +         // timezone for event
294                "duration TEXT," +
295                "allDay INTEGER NOT NULL DEFAULT 0," +
296                "visibility INTEGER NOT NULL DEFAULT 0," +
297                "transparency INTEGER NOT NULL DEFAULT 0," +
298                "hasAlarm INTEGER NOT NULL DEFAULT 0," +
299                "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
300                "rrule TEXT," +
301                "rdate TEXT," +
302                "exrule TEXT," +
303                "exdate TEXT," +
304                "originalEvent TEXT," +  // _sync_id of recurring event
305                "originalInstanceTime INTEGER," +  // millis since epoch
306                "originalAllDay INTEGER," +
307                "lastDate INTEGER," +               // millis since epoch
308                "hasAttendeeData INTEGER NOT NULL DEFAULT 0," +
309                "guestsCanModify INTEGER NOT NULL DEFAULT 0," +
310                "guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," +
311                "guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," +
312                "organizer STRING," +
313                "deleted INTEGER NOT NULL DEFAULT 0," +
314                "dtstart2 INTEGER," + //millis since epoch, allDay events in local timezone
315                "dtend2 INTEGER," + //millis since epoch, allDay events in local timezone
316                "eventTimezone2 TEXT," + //timezone for event with allDay events in local timezone
317                "syncAdapterData TEXT" + //available for use by sync adapters
318                ");");
319    }
320
321    private void addVersion50Events() {
322        // April 5th 1:01:01 AM to April 6th 1:01:01
323        mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration," +
324                "eventTimezone,allDay,calendar_id) " +
325                "VALUES (1,1270454471000,1270540872000,'P10S'," +
326                "'America/Los_Angeles',1,1);");
327
328        // April 5th midnight to April 6th midnight, duration cleared
329        mGoodDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration," +
330                "eventTimezone,allDay,calendar_id) " +
331                "VALUES (1,1270425600000,1270512000000,null," +
332                "'UTC',1,1);");
333
334        // April 5th 1:01:01 AM to April 6th 1:01:01, recurring weekly (We only check for the
335        // existence of an rrule so it doesn't matter if the day is correct)
336        mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration," +
337                "eventTimezone,allDay,rrule,calendar_id) " +
338                "VALUES (2,1270454462000,1270540863000," +
339                "'P10S','America/Los_Angeles',1," +
340                "'WEEKLY:MON',1);");
341
342        // April 5th midnight with 1 day duration, if only dtend was wrong we wouldn't fix it, but
343        // if anything else is wrong we clear dtend to be sure.
344        mGoodDb.execSQL("INSERT INTO Events (" +
345                "_id,dtstart,dtend,duration," +
346                "eventTimezone,allDay,rrule,calendar_id)" +
347                "VALUES (2,1270425600000,null,'P1D'," +
348                "'UTC',1," +
349                "'WEEKLY:MON',1);");
350
351        assertEquals(mBadDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2);
352        assertEquals(mGoodDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2);
353    }
354
355    private void addVersion67Events() {
356        // April 5th 1:01:01 AM to April 6th 1:01:01
357        mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration,dtstart2,dtend2," +
358                "eventTimezone,eventTimezone2,allDay,calendar_id) " +
359                "VALUES (1,1270454471000,1270540872000,'P10S'," +
360                "1270454460000,1270540861000,'America/Los_Angeles','America/Los_Angeles',1,1);");
361
362        // April 5th midnight to April 6th midnight, duration cleared
363        mGoodDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration,dtstart2,dtend2," +
364                "eventTimezone,eventTimezone2,allDay,calendar_id) " +
365                "VALUES (1,1270425600000,1270512000000,null," +
366                "1270450800000,1270537200000,'UTC','America/Los_Angeles',1,1);");
367
368        // April 5th 1:01:01 AM to April 6th 1:01:01, recurring weekly (We only check for the
369        // existence of an rrule so it doesn't matter if the day is correct)
370        mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration,dtstart2,dtend2," +
371                "eventTimezone,eventTimezone2,allDay,rrule,calendar_id) " +
372                "VALUES (2,1270454462000,1270540863000," +
373                "'P10S',1270454461000,1270540861000,'America/Los_Angeles','America/Los_Angeles',1," +
374                "'WEEKLY:MON',1);");
375
376        // April 5th midnight with 1 day duration, if only dtend was wrong we wouldn't fix it, but
377        // if anything else is wrong we clear dtend to be sure.
378        mGoodDb.execSQL("INSERT INTO Events (" +
379                "_id,dtstart,dtend,duration,dtstart2,dtend2," +
380                "eventTimezone,eventTimezone2,allDay,rrule,calendar_id)" +
381                "VALUES (2,1270425600000,null,'P1D',1270450800000,null," +
382                "'UTC','America/Los_Angeles',1," +
383                "'WEEKLY:MON',1);");
384
385        assertEquals(mBadDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2);
386        assertEquals(mGoodDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2);
387    }
388
389    @MediumTest
390    public void testUpgradeToVersion69() {
391        // Create event tables
392        createVersion67EventsTable(mBadDb);
393        createVersion67EventsTable(mGoodDb);
394        // Fill in good and bad events
395        addVersion67Events();
396        // Run the upgrade on the bad events
397        CalendarDatabaseHelper.upgradeToVersion69(mBadDb);
398        Cursor badCursor = null;
399        Cursor goodCursor = null;
400        try {
401            badCursor = mBadDb.rawQuery("SELECT _id,dtstart,dtend,duration,dtstart2,dtend2," +
402                    "eventTimezone,eventTimezone2,rrule FROM Events WHERE allDay=?",
403                    new String[] {"1"});
404            goodCursor = mGoodDb.rawQuery("SELECT _id,dtstart,dtend,duration,dtstart2,dtend2," +
405                    "eventTimezone,eventTimezone2,rrule FROM Events WHERE allDay=?",
406                    new String[] {"1"});
407            // Check that we get the correct results back
408            assertTrue(compareCursors(badCursor, goodCursor));
409        } finally {
410            if (badCursor != null) {
411                badCursor.close();
412            }
413            if (goodCursor != null) {
414                goodCursor.close();
415            }
416        }
417    }
418
419    @MediumTest
420    public void testUpgradeToCurrentVersion() {
421        // Create event tables
422        bootstrapDbVersion50(mBadDb);
423        bootstrapDbVersion50(mGoodDb);
424        // Fill in good and bad events
425        addVersion50Events();
426        // Run the upgrade on the bad events
427        CalendarDatabaseHelper cDbHelper = new CalendarDatabaseHelper(new MockContext());
428        cDbHelper.mInTestMode = true;
429        cDbHelper.onUpgrade(mBadDb, 50, CalendarDatabaseHelper.DATABASE_VERSION);
430        Cursor badCursor = null;
431        Cursor goodCursor = null;
432        try {
433            badCursor = mBadDb.rawQuery("SELECT _id,dtstart,dtend,duration," +
434                    "eventTimezone,rrule FROM Events WHERE allDay=?",
435                    new String[] {"1"});
436            goodCursor = mGoodDb.rawQuery("SELECT _id,dtstart,dtend,duration," +
437                    "eventTimezone,rrule FROM Events WHERE allDay=?",
438                    new String[] {"1"});
439            // Check that we get the correct results back
440            assertTrue(compareCursors(badCursor, goodCursor));
441        } finally {
442            if (badCursor != null) {
443                badCursor.close();
444            }
445            if (goodCursor != null) {
446                goodCursor.close();
447            }
448        }
449    }
450
451    private static final String SQLITE_MASTER = "sqlite_master";
452
453    private static final String[] PROJECTION = {"tbl_name", "sql"};
454
455    public void testSchemasEqualForAllTables() {
456
457        CalendarDatabaseHelper cDbHelper = new CalendarDatabaseHelper(new MockContext());
458        cDbHelper.mInTestMode = true;
459        bootstrapDbVersion50(mBadDb);
460        cDbHelper.onCreate(mGoodDb);
461        cDbHelper.onUpgrade(mBadDb, 50, CalendarDatabaseHelper.DATABASE_VERSION);
462        // Check that for all tables, schema definitions are the same between updated db and new db.
463        Cursor goodCursor = mGoodDb.query(SQLITE_MASTER, PROJECTION, null, null, null, null,
464                "tbl_name,sql" /* orderBy */);
465        Cursor badCursor = mBadDb.query(SQLITE_MASTER, PROJECTION, null, null, null, null,
466                "tbl_name,sql" /* orderBy */);
467
468        while (goodCursor.moveToNext()) {
469            String goodTableName = goodCursor.getString(0);
470            // Ignore tables that do not belong to calendar
471            if (goodTableName.startsWith("sqlite_") || goodTableName.equals("android_metadata")) {
472                continue;
473            }
474
475            // Ignore tables that do not belong to calendar
476            String badTableName;
477            do {
478                assertTrue("Should have same number of tables", badCursor.moveToNext());
479                badTableName = badCursor.getString(0);
480            } while (badTableName.startsWith("sqlite_") || badTableName.equals("android_metadata"));
481
482            assertEquals("Table names different between upgraded schema and freshly-created scheme",
483                    goodTableName, badTableName);
484
485            String badString = badCursor.getString(1);
486            String goodString = goodCursor.getString(1);
487            if (badString == null && goodString == null) {
488                continue;
489            }
490            // Have to strip out some special characters and collapse spaces to
491            // get reasonable output
492            badString = badString.replaceAll("[()]", "");
493            goodString = goodString.replaceAll("[()]", "");
494            badString = badString.replaceAll(" +", " ");
495            goodString = goodString.replaceAll(" +", " ");
496            // And then split on commas and trim whitespace
497            String[] badSql = badString.split(",");
498            String[] goodSql = goodString.split(",");
499            for (int i = 0; i < badSql.length; i++) {
500                badSql[i] = badSql[i].trim();
501            }
502            for (int i = 0; i < goodSql.length; i++) {
503                goodSql[i] = goodSql[i].trim();
504            }
505            Arrays.sort(badSql);
506            Arrays.sort(goodSql);
507            assertTrue("Table schema different for table " + goodCursor.getString(0) + ": <"
508                    + Arrays.toString(goodSql) + "> -- <" + Arrays.toString(badSql) + ">",
509                    Arrays.equals(goodSql, badSql));
510        }
511        assertFalse("Should have same number of tables", badCursor.moveToNext());
512    }
513
514    /**
515     * Compares two cursors to see if they contain the same data.
516     *
517     * @return Returns true of the cursors contain the same data and are not null, false
518     * otherwise
519     */
520    private static boolean compareCursors(Cursor c1, Cursor c2) {
521        if(c1 == null || c2 == null) {
522            Log.d("CDBT","c1 is " + c1 + " and c2 is " + c2);
523            return false;
524        }
525
526        int numColumns = c1.getColumnCount();
527        if (numColumns != c2.getColumnCount()) {
528            Log.d("CDBT","c1 has " + numColumns + " columns and c2 has " + c2.getColumnCount());
529            return false;
530        }
531
532        if (c1.getCount() != c2.getCount()) {
533            Log.d("CDBT","c1 has " + c1.getCount() + " rows and c2 has " + c2.getCount());
534            return false;
535        }
536
537        c1.moveToPosition(-1);
538        c2.moveToPosition(-1);
539        while(c1.moveToNext() && c2.moveToNext()) {
540            for(int i = 0; i < numColumns; i++) {
541                if(!TextUtils.equals(c1.getString(i),c2.getString(i))) {
542                    Log.d("CDBT", c1.getString(i) + "\n" + c2.getString(i));
543                    return false;
544                }
545            }
546        }
547
548        return true;
549    }
550}
551