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