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  */
16 package com.android.providers.calendar;
17 
18 
19 import com.android.common.content.SyncStateContentProviderHelper;
20 
21 import android.database.Cursor;
22 import android.database.DatabaseUtils;
23 import android.database.sqlite.SQLiteDatabase;
24 import android.test.mock.MockContext;
25 import android.test.suitebuilder.annotation.MediumTest;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import java.util.Arrays;
30 
31 import junit.framework.TestCase;
32 
33 public 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
setUp()42     public void setUp() {
43         mBadDb = SQLiteDatabase.create(null);
44         assertNotNull(mBadDb);
45         mGoodDb = SQLiteDatabase.create(null);
46         assertNotNull(mGoodDb);
47     }
48 
bootstrapDbVersion50(SQLiteDatabase db)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 
createVersion67EventsTable(SQLiteDatabase db)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 
addVersion50Events()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 
addVersion67Events()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
testUpgradeToVersion69()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
testUpgradeToCurrentVersion()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 
testSchemasEqualForAllTables()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      */
compareCursors(Cursor c1, Cursor c2)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