blob: a2925d4724d6fb9cebeadf71f5343f8357493821 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.providers.calendar;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.test.mock.MockContext;
import android.text.TextUtils;
import android.util.Log;
import androidx.test.runner.AndroidJUnit4;
import com.android.common.content.SyncStateContentProviderHelper;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
public class CalendarDatabaseHelperTest {
private static final String TAG = "CDbHelperTest";
private SQLiteDatabase mBadDb;
private SQLiteDatabase mGoodDb;
private DatabaseUtils.InsertHelper mBadEventsInserter;
private DatabaseUtils.InsertHelper mGoodEventsInserter;
@Before
public void setUp() {
mBadDb = SQLiteDatabase.create(null);
assertNotNull(mBadDb);
mGoodDb = SQLiteDatabase.create(null);
assertNotNull(mGoodDb);
}
protected void bootstrapDbVersion50(SQLiteDatabase db) {
// TODO remove the dependency on this system class
SyncStateContentProviderHelper syncStateHelper = new SyncStateContentProviderHelper();
syncStateHelper.createDatabase(db);
db.execSQL("CREATE TABLE Calendars (" +
"_id INTEGER PRIMARY KEY," +
"_sync_account TEXT," +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_time TEXT," + // UTC
"_sync_local_id INTEGER," +
"_sync_dirty INTEGER," +
"_sync_mark INTEGER," + // Used to filter out new rows
"url TEXT," +
"name TEXT," +
"displayName TEXT," +
"hidden INTEGER NOT NULL DEFAULT 0," +
"color INTEGER," +
"access_level INTEGER," +
"selected INTEGER NOT NULL DEFAULT 1," +
"sync_events INTEGER NOT NULL DEFAULT 0," +
"location TEXT," +
"timezone TEXT" +
");");
// Trigger to remove a calendar's events when we delete the calendar
db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
"BEGIN " +
"DELETE FROM Events WHERE calendar_id = old._id;" +
"DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
"END");
// TODO: do we need both dtend and duration?
db.execSQL("CREATE TABLE Events (" +
"_id INTEGER PRIMARY KEY," +
"_sync_account TEXT," +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_time TEXT," + // UTC
"_sync_local_id INTEGER," +
"_sync_dirty INTEGER," +
"_sync_mark INTEGER," + // To filter out new rows
// TODO remove NOT NULL when upgrade rebuilds events to have
// true v50 schema
"calendar_id INTEGER NOT NULL," +
"htmlUri TEXT," +
"title TEXT," +
"eventLocation TEXT," +
"description TEXT," +
"eventStatus INTEGER," +
"selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
"commentsUri TEXT," +
"dtstart INTEGER," + // millis since epoch
"dtend INTEGER," + // millis since epoch
"eventTimezone TEXT," + // timezone for event
"duration TEXT," +
"allDay INTEGER NOT NULL DEFAULT 0," +
"visibility INTEGER NOT NULL DEFAULT 0," +
"transparency INTEGER NOT NULL DEFAULT 0," +
"hasAlarm INTEGER NOT NULL DEFAULT 0," +
"hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
"rrule TEXT," +
"rdate TEXT," +
"exrule TEXT," +
"exdate TEXT," +
"originalEvent TEXT," +
"originalInstanceTime INTEGER," + // millis since epoch
"lastDate INTEGER" + // millis since epoch
");");
db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (calendar_id);");
db.execSQL("CREATE TABLE EventsRawTimes (" +
"_id INTEGER PRIMARY KEY," +
"event_id INTEGER NOT NULL," +
"dtstart2445 TEXT," +
"dtend2445 TEXT," +
"originalInstanceTime2445 TEXT," +
"lastDate2445 TEXT," +
"UNIQUE (event_id)" +
");");
// NOTE: we do not create a trigger to delete an event's instances upon update,
// as all rows currently get updated during a merge.
db.execSQL("CREATE TABLE DeletedEvents (" +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_account TEXT," +
"_sync_mark INTEGER" + // To filter out new rows
");");
db.execSQL("CREATE TABLE Instances (" +
"_id INTEGER PRIMARY KEY," +
"event_id INTEGER," +
"begin INTEGER," + // UTC millis
"end INTEGER," + // UTC millis
"startDay INTEGER," + // Julian start day
"endDay INTEGER," + // Julian end day
"startMinute INTEGER," + // minutes from midnight
"endMinute INTEGER," + // minutes from midnight
"UNIQUE (event_id, begin, end)" +
");");
db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (startDay);");
db.execSQL("CREATE TABLE CalendarMetaData (" +
"_id INTEGER PRIMARY KEY," +
"localTimezone TEXT," +
"minInstance INTEGER," + // UTC millis
"maxInstance INTEGER," + // UTC millis
"minBusyBits INTEGER," + // UTC millis
"maxBusyBits INTEGER" + // UTC millis
");");
db.execSQL("CREATE TABLE BusyBits(" +
"day INTEGER PRIMARY KEY," + // the Julian day
"busyBits INTEGER," + // 24 bits for 60-minute intervals
"allDayCount INTEGER" + // number of all-day events
");");
db.execSQL("CREATE TABLE Attendees (" +
"_id INTEGER PRIMARY KEY," +
"event_id INTEGER," +
"attendeeName TEXT," +
"attendeeEmail TEXT," +
"attendeeStatus INTEGER," +
"attendeeRelationship INTEGER," +
"attendeeType INTEGER" +
");");
db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (event_id);");
db.execSQL("CREATE TABLE Reminders (" +
"_id INTEGER PRIMARY KEY," +
"event_id INTEGER," +
"minutes INTEGER," +
"method INTEGER NOT NULL" +
" DEFAULT 0);");
db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (event_id);");
// This table stores the Calendar notifications that have gone off.
db.execSQL("CREATE TABLE CalendarAlerts (" +
"_id INTEGER PRIMARY KEY," +
"event_id INTEGER," +
"begin INTEGER NOT NULL," + // UTC millis
"end INTEGER NOT NULL," + // UTC millis
"alarmTime INTEGER NOT NULL," + // UTC millis
"state INTEGER NOT NULL," +
"minutes INTEGER," +
"UNIQUE (alarmTime, begin, event_id)" +
");");
db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (event_id);");
db.execSQL("CREATE TABLE ExtendedProperties (" +
"_id INTEGER PRIMARY KEY," +
"event_id INTEGER," +
"name TEXT," +
"value TEXT" +
");");
db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (event_id);");
// Trigger to remove data tied to an event when we delete that event.
db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
"BEGIN " +
"DELETE FROM Instances WHERE event_id = old._id;" +
"DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
"DELETE FROM Attendees WHERE event_id = old._id;" +
"DELETE FROM Reminders WHERE event_id = old._id;" +
"DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
"DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
"END");
// Triggers to set the _sync_dirty flag when an attendee is changed,
// inserted or deleted
db.execSQL("CREATE TRIGGER attendees_update UPDATE ON Attendees " +
"BEGIN " +
"UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
"END");
db.execSQL("CREATE TRIGGER attendees_insert INSERT ON Attendees " +
"BEGIN " +
"UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
"END");
db.execSQL("CREATE TRIGGER attendees_delete DELETE ON Attendees " +
"BEGIN " +
"UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
"END");
// Triggers to set the _sync_dirty flag when a reminder is changed,
// inserted or deleted
db.execSQL("CREATE TRIGGER reminders_update UPDATE ON Reminders " +
"BEGIN " +
"UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
"END");
db.execSQL("CREATE TRIGGER reminders_insert INSERT ON Reminders " +
"BEGIN " +
"UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
"END");
db.execSQL("CREATE TRIGGER reminders_delete DELETE ON Reminders " +
"BEGIN " +
"UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
"END");
// Triggers to set the _sync_dirty flag when an extended property is changed,
// inserted or deleted
db.execSQL("CREATE TRIGGER extended_properties_update UPDATE ON ExtendedProperties " +
"BEGIN " +
"UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
"END");
db.execSQL("CREATE TRIGGER extended_properties_insert UPDATE ON ExtendedProperties " +
"BEGIN " +
"UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
"END");
db.execSQL("CREATE TRIGGER extended_properties_delete UPDATE ON ExtendedProperties " +
"BEGIN " +
"UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
"END");
}
private void createVersion67EventsTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE Events (" +
"_id INTEGER PRIMARY KEY," +
"_sync_account TEXT," +
"_sync_account_type TEXT," +
"_sync_id TEXT," +
"_sync_version TEXT," +
"_sync_time TEXT," + // UTC
"_sync_local_id INTEGER," +
"_sync_dirty INTEGER," +
"_sync_mark INTEGER," + // To filter out new rows
"calendar_id INTEGER NOT NULL," +
"htmlUri TEXT," +
"title TEXT," +
"eventLocation TEXT," +
"description TEXT," +
"eventStatus INTEGER," +
"selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
"commentsUri TEXT," +
"dtstart INTEGER," + // millis since epoch
"dtend INTEGER," + // millis since epoch
"eventTimezone TEXT," + // timezone for event
"duration TEXT," +
"allDay INTEGER NOT NULL DEFAULT 0," +
"visibility INTEGER NOT NULL DEFAULT 0," +
"transparency INTEGER NOT NULL DEFAULT 0," +
"hasAlarm INTEGER NOT NULL DEFAULT 0," +
"hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
"rrule TEXT," +
"rdate TEXT," +
"exrule TEXT," +
"exdate TEXT," +
"originalEvent TEXT," + // _sync_id of recurring event
"originalInstanceTime INTEGER," + // millis since epoch
"originalAllDay INTEGER," +
"lastDate INTEGER," + // millis since epoch
"hasAttendeeData INTEGER NOT NULL DEFAULT 0," +
"guestsCanModify INTEGER NOT NULL DEFAULT 0," +
"guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," +
"guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," +
"organizer STRING," +
"deleted INTEGER NOT NULL DEFAULT 0," +
"dtstart2 INTEGER," + //millis since epoch, allDay events in local timezone
"dtend2 INTEGER," + //millis since epoch, allDay events in local timezone
"eventTimezone2 TEXT," + //timezone for event with allDay events in local timezone
"syncAdapterData TEXT" + //available for use by sync adapters
");");
}
private void addVersion50Events() {
// April 5th 1:01:01 AM to April 6th 1:01:01
mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration," +
"eventTimezone,allDay,calendar_id) " +
"VALUES (1,1270454471000,1270540872000,'P10S'," +
"'America/Los_Angeles',1,1);");
// April 5th midnight to April 6th midnight, duration cleared
mGoodDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration," +
"eventTimezone,allDay,calendar_id) " +
"VALUES (1,1270425600000,1270512000000,null," +
"'UTC',1,1);");
// April 5th 1:01:01 AM to April 6th 1:01:01, recurring weekly (We only check for the
// existence of an rrule so it doesn't matter if the day is correct)
mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration," +
"eventTimezone,allDay,rrule,calendar_id) " +
"VALUES (2,1270454462000,1270540863000," +
"'P10S','America/Los_Angeles',1," +
"'WEEKLY:MON',1);");
// April 5th midnight with 1 day duration, if only dtend was wrong we wouldn't fix it, but
// if anything else is wrong we clear dtend to be sure.
mGoodDb.execSQL("INSERT INTO Events (" +
"_id,dtstart,dtend,duration," +
"eventTimezone,allDay,rrule,calendar_id)" +
"VALUES (2,1270425600000,null,'P1D'," +
"'UTC',1," +
"'WEEKLY:MON',1);");
assertEquals(mBadDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2);
assertEquals(mGoodDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2);
}
private void addVersion67Events() {
// April 5th 1:01:01 AM to April 6th 1:01:01
mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration,dtstart2,dtend2," +
"eventTimezone,eventTimezone2,allDay,calendar_id) " +
"VALUES (1,1270454471000,1270540872000,'P10S'," +
"1270454460000,1270540861000,'America/Los_Angeles','America/Los_Angeles',1,1);");
// April 5th midnight to April 6th midnight, duration cleared
mGoodDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration,dtstart2,dtend2," +
"eventTimezone,eventTimezone2,allDay,calendar_id) " +
"VALUES (1,1270425600000,1270512000000,null," +
"1270450800000,1270537200000,'UTC','America/Los_Angeles',1,1);");
// April 5th 1:01:01 AM to April 6th 1:01:01, recurring weekly (We only check for the
// existence of an rrule so it doesn't matter if the day is correct)
mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration,dtstart2,dtend2," +
"eventTimezone,eventTimezone2,allDay,rrule,calendar_id) " +
"VALUES (2,1270454462000,1270540863000," +
"'P10S',1270454461000,1270540861000,'America/Los_Angeles','America/Los_Angeles',1," +
"'WEEKLY:MON',1);");
// April 5th midnight with 1 day duration, if only dtend was wrong we wouldn't fix it, but
// if anything else is wrong we clear dtend to be sure.
mGoodDb.execSQL("INSERT INTO Events (" +
"_id,dtstart,dtend,duration,dtstart2,dtend2," +
"eventTimezone,eventTimezone2,allDay,rrule,calendar_id)" +
"VALUES (2,1270425600000,null,'P1D',1270450800000,null," +
"'UTC','America/Los_Angeles',1," +
"'WEEKLY:MON',1);");
assertEquals(mBadDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2);
assertEquals(mGoodDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2);
}
@Test
public void testUpgradeToVersion69() {
// Create event tables
createVersion67EventsTable(mBadDb);
createVersion67EventsTable(mGoodDb);
// Fill in good and bad events
addVersion67Events();
// Run the upgrade on the bad events
CalendarDatabaseHelper.upgradeToVersion69(mBadDb);
Cursor badCursor = null;
Cursor goodCursor = null;
try {
badCursor = mBadDb.rawQuery("SELECT _id,dtstart,dtend,duration,dtstart2,dtend2," +
"eventTimezone,eventTimezone2,rrule FROM Events WHERE allDay=?",
new String[] {"1"});
goodCursor = mGoodDb.rawQuery("SELECT _id,dtstart,dtend,duration,dtstart2,dtend2," +
"eventTimezone,eventTimezone2,rrule FROM Events WHERE allDay=?",
new String[] {"1"});
// Check that we get the correct results back
assertTrue(compareCursors(badCursor, goodCursor));
} finally {
if (badCursor != null) {
badCursor.close();
}
if (goodCursor != null) {
goodCursor.close();
}
}
}
@Test
@Ignore("b/140236227")
public void testUpgradeToCurrentVersion() {
// Create event tables
bootstrapDbVersion50(mBadDb);
bootstrapDbVersion50(mGoodDb);
// Fill in good and bad events
addVersion50Events();
// Run the upgrade on the bad events
CalendarDatabaseHelper cDbHelper = new CalendarDatabaseHelper(new MockContext());
cDbHelper.mInTestMode = true;
cDbHelper.onUpgrade(mBadDb, 50, CalendarDatabaseHelper.DATABASE_VERSION);
Cursor badCursor = null;
Cursor goodCursor = null;
try {
badCursor = mBadDb.rawQuery("SELECT _id,dtstart,dtend,duration," +
"eventTimezone,rrule FROM Events WHERE allDay=?",
new String[] {"1"});
goodCursor = mGoodDb.rawQuery("SELECT _id,dtstart,dtend,duration," +
"eventTimezone,rrule FROM Events WHERE allDay=?",
new String[] {"1"});
// Check that we get the correct results back
assertTrue(compareCursors(badCursor, goodCursor));
} finally {
if (badCursor != null) {
badCursor.close();
}
if (goodCursor != null) {
goodCursor.close();
}
}
}
private static final String SQLITE_MASTER = "sqlite_master";
private static final String[] PROJECTION = {"tbl_name", "sql"};
@Test
@Ignore("b/140236227")
public void testSchemasEqualForAllTables() {
CalendarDatabaseHelper cDbHelper = new CalendarDatabaseHelper(new MockContext());
cDbHelper.mInTestMode = true;
bootstrapDbVersion50(mBadDb);
cDbHelper.onCreate(mGoodDb);
cDbHelper.onUpgrade(mBadDb, 50, CalendarDatabaseHelper.DATABASE_VERSION);
// Check that for all tables, schema definitions are the same between updated db and new db.
Cursor goodCursor = mGoodDb.query(SQLITE_MASTER, PROJECTION, null, null, null, null,
"tbl_name,sql" /* orderBy */);
Cursor badCursor = mBadDb.query(SQLITE_MASTER, PROJECTION, null, null, null, null,
"tbl_name,sql" /* orderBy */);
while (goodCursor.moveToNext()) {
String goodTableName = goodCursor.getString(0);
// Ignore tables that do not belong to calendar
if (goodTableName.startsWith("sqlite_") || goodTableName.equals("android_metadata")) {
continue;
}
// Ignore tables that do not belong to calendar
String badTableName;
do {
assertTrue("Should have same number of tables", badCursor.moveToNext());
badTableName = badCursor.getString(0);
} while (badTableName.startsWith("sqlite_") || badTableName.equals("android_metadata"));
assertEquals("Table names different between upgraded schema and freshly-created scheme",
goodTableName, badTableName);
String badString = badCursor.getString(1);
String goodString = goodCursor.getString(1);
if (badString == null && goodString == null) {
continue;
}
// Have to strip out some special characters and collapse spaces to
// get reasonable output
badString = badString.replaceAll("[()]", "");
goodString = goodString.replaceAll("[()]", "");
badString = badString.replaceAll(" +", " ");
goodString = goodString.replaceAll(" +", " ");
// And then split on commas and trim whitespace
String[] badSql = badString.split(",");
String[] goodSql = goodString.split(",");
for (int i = 0; i < badSql.length; i++) {
badSql[i] = badSql[i].trim();
}
for (int i = 0; i < goodSql.length; i++) {
goodSql[i] = goodSql[i].trim();
}
Arrays.sort(badSql);
Arrays.sort(goodSql);
assertTrue("Table schema different for table " + goodCursor.getString(0) + ": <"
+ Arrays.toString(goodSql) + "> -- <" + Arrays.toString(badSql) + ">",
Arrays.equals(goodSql, badSql));
}
assertFalse("Should have same number of tables", badCursor.moveToNext());
}
/**
* Compares two cursors to see if they contain the same data.
*
* @return Returns true of the cursors contain the same data and are not null, false
* otherwise
*/
private static boolean compareCursors(Cursor c1, Cursor c2) {
if(c1 == null || c2 == null) {
Log.d("CDBT","c1 is " + c1 + " and c2 is " + c2);
return false;
}
int numColumns = c1.getColumnCount();
if (numColumns != c2.getColumnCount()) {
Log.d("CDBT","c1 has " + numColumns + " columns and c2 has " + c2.getColumnCount());
return false;
}
if (c1.getCount() != c2.getCount()) {
Log.d("CDBT","c1 has " + c1.getCount() + " rows and c2 has " + c2.getCount());
return false;
}
c1.moveToPosition(-1);
c2.moveToPosition(-1);
while(c1.moveToNext() && c2.moveToNext()) {
for(int i = 0; i < numColumns; i++) {
if(!TextUtils.equals(c1.getString(i),c2.getString(i))) {
Log.d("CDBT", c1.getString(i) + "\n" + c2.getString(i));
return false;
}
}
}
return true;
}
}