Home | History | Annotate | Download | only in calendar
      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
     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