CalendarProvider2Test.java revision 1ae4c22f15c107cd9f9cd8babaa11005e45e4647
1810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal/* 2810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * Copyright (C) 2008 The Android Open Source Project 3810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * 4810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * Licensed under the Apache License, Version 2.0 (the "License"); 5810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * you may not use this file except in compliance with the License. 6810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * You may obtain a copy of the License at 7810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * 8810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * http://www.apache.org/licenses/LICENSE-2.0 9810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * 10810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * Unless required by applicable law or agreed to in writing, software 11810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * distributed under the License is distributed on an "AS IS" BASIS, 12810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * See the License for the specific language governing permissions and 14810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal * limitations under the License. 15810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal */ 16810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal 177cc70b4f0ad1064a4a0dce6056ad82b205887160Tyler Gunnpackage com.android.providers.calendar; 18810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal 19e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Leeimport com.android.common.ArrayListCursor; 20e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee 21810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepalimport android.content.*; 22810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepalimport android.database.Cursor; 23e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Leeimport android.database.sqlite.SQLiteDatabase; 24e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Leeimport android.net.Uri; 25810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepalimport android.text.format.DateUtils; 26810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepalimport android.text.format.Time; 27810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepalimport android.provider.Calendar; 28810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepalimport android.provider.Calendar.BusyBits; 29810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepalimport android.provider.Calendar.Calendars; 30e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Leeimport android.provider.Calendar.Events; 318de76915ea2772faeb41705aaaeb65f5b3478ac4Ihab Awadimport android.provider.Calendar.EventsEntity; 32e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Leeimport android.provider.Calendar.Instances; 33e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Leeimport android.test.ProviderTestCase2; 34e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Leeimport android.test.mock.MockContentResolver; 358de76915ea2772faeb41705aaaeb65f5b3478ac4Ihab Awadimport android.test.suitebuilder.annotation.LargeTest; 368de76915ea2772faeb41705aaaeb65f5b3478ac4Ihab Awadimport android.test.suitebuilder.annotation.Suppress; 37e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Leeimport android.util.Log; 38e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee 39e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Leeimport java.util.ArrayList; 40e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee 41e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee/** 42e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee * Runs various tests on an isolated Calendar provider with its own database. 43e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee */ 44810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal@LargeTest 456fb37c87836b5245046bd3b14320823ab839a10cIhab Awadpublic class CalendarProvider2Test extends ProviderTestCase2<CalendarProvider2ForTesting> { 46f15dc33f87f88e21ef745952a68af65c86e1bf1eTyler Gunn static final String TAG = "calendar"; 47f15dc33f87f88e21ef745952a68af65c86e1bf1eTyler Gunn static final String DEFAULT_TIMEZONE = "America/Los_Angeles"; 48f15dc33f87f88e21ef745952a68af65c86e1bf1eTyler Gunn 49880b98372368b1d2be2ed34edaeb1e2f338d121aSantos Cordon private SQLiteDatabase mDb; 50810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal private MetaData mMetaData; 51810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal private Context mContext; 52e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee private MockContentResolver mResolver; 53e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee private Uri mEventsUri = Events.CONTENT_URI; 541a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn private int mCalendarId; 551a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn 561a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn protected boolean mWipe = false; 57880b98372368b1d2be2ed34edaeb1e2f338d121aSantos Cordon 58880b98372368b1d2be2ed34edaeb1e2f338d121aSantos Cordon // We need a unique id to put in the _sync_id field so that we can create 59810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal // recurrence exceptions that refer to recurring events. 60e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee private int mGlobalSyncId = 1000; 61e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee private static final String CALENDAR_URL = 621a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn "http://www.google.com/calendar/feeds/joe%40joe.com/private/full"; 631a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn 641a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn /** 651a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn * KeyValue is a simple class that stores a pair of strings representing 661a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn * a (key, value) pair. This is used for updating events. 671a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn */ 681a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn private class KeyValue { 691a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn String key; 701a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn String value; 711a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn 721a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn public KeyValue(String key, String value) { 731a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn this.key = key; 741a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn this.value = value; 751a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn } 761a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn } 771a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn 78880b98372368b1d2be2ed34edaeb1e2f338d121aSantos Cordon /** 79880b98372368b1d2be2ed34edaeb1e2f338d121aSantos Cordon * A generic command interface. This is used to support a sequence of 80880b98372368b1d2be2ed34edaeb1e2f338d121aSantos Cordon * commands that can create events, delete or update events, and then 81880b98372368b1d2be2ed34edaeb1e2f338d121aSantos Cordon * check that the state of the database is as expected. 82677e811379dba9b0cce2230b770697d474ed8e56Yorke Lee */ 83677e811379dba9b0cce2230b770697d474ed8e56Yorke Lee private interface Command { 841a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn public void execute(); 851a40c4f4daf2cdfba3f67383c726f93b85a98b00Tyler Gunn } 86677e811379dba9b0cce2230b770697d474ed8e56Yorke Lee 878de76915ea2772faeb41705aaaeb65f5b3478ac4Ihab Awad /** 88677e811379dba9b0cce2230b770697d474ed8e56Yorke Lee * This is used to insert a new event into the database. The event is 891e37be5dd86a51b90e461f09dc8a89effe4aee21Tyler Gunn * specified by its name (or "title"). All of the event fields (the 901e37be5dd86a51b90e461f09dc8a89effe4aee21Tyler Gunn * start and end time, whether it is an all-day event, and so on) are 91677e811379dba9b0cce2230b770697d474ed8e56Yorke Lee * stored in a separate table (the "mEvents" table). 92e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee */ 93677e811379dba9b0cce2230b770697d474ed8e56Yorke Lee private class Insert implements Command { 94e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee EventInfo eventInfo; 95810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal 96f86db2e63ec71781ac8214ed6d2603d02266143dYorke Lee public Insert(String eventName) { 97f86db2e63ec71781ac8214ed6d2603d02266143dYorke Lee eventInfo = findEvent(eventName); 98f86db2e63ec71781ac8214ed6d2603d02266143dYorke Lee } 99f86db2e63ec71781ac8214ed6d2603d02266143dYorke Lee 100e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee public void execute() { 101e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee Log.i(TAG, "insert " + eventInfo.mTitle); 102e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee insertEvent(mCalendarId, eventInfo); 10312de5495028142b15c5e25508c77ff4506e29b57Ihab Awad } 104810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal } 105e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee 106e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee /** 107e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee * This is used to delete an event, specified by the event name. 108e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee */ 109e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee private class Delete implements Command { 110810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal String eventName; 11153fb8c103ab542608e0225eae1edfdf43bfa2273Sailesh Nepal int expected; 112e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee 113e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee public Delete(String eventName, int expected) { 114e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee this.eventName = eventName; 115e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee this.expected = expected; 11653fb8c103ab542608e0225eae1edfdf43bfa2273Sailesh Nepal } 117e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee 118e19d33e309b1024261c085ddc1ee1e438442bb14Yorke Lee public void execute() { 11953fb8c103ab542608e0225eae1edfdf43bfa2273Sailesh Nepal Log.i(TAG, "delete " + eventName); 120810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal int rows = deleteMatchingEvents(eventName); 121810735e3f0a4fe924a805981d32b6916ec834b38Sailesh Nepal assertEquals(expected, rows); 122 } 123 } 124 125 /** 126 * This is used to update an event. The values to update are specified 127 * with an array of (key, value) pairs. Both the key and value are 128 * specified as strings. Event fields that are not really strings (such 129 * as DTSTART which is a long) should be converted to the appropriate type 130 * but that isn't supported yet. When needed, that can be added here 131 * by checking for specific keys and converting the associated values. 132 */ 133 private class Update implements Command { 134 String eventName; 135 KeyValue[] pairs; 136 137 public Update(String eventName, KeyValue[] pairs) { 138 this.eventName = eventName; 139 this.pairs = pairs; 140 } 141 142 public void execute() { 143 Log.i(TAG, "update " + eventName); 144 if (mWipe) { 145 // Wipe instance table so it will be regenerated 146 mMetaData.clearInstanceRange(); 147 } 148 ContentValues map = new ContentValues(); 149 for (KeyValue pair : pairs) { 150 String value = pair.value; 151 if (Calendar.EventsColumns.STATUS.equals(pair.key)) { 152 // Do type conversion for STATUS 153 map.put(pair.key, Integer.parseInt(value)); 154 } else { 155 map.put(pair.key, value); 156 } 157 } 158 updateMatchingEvents(eventName, map); 159 } 160 } 161 162 /** 163 * This command queries the number of events and compares it to the given 164 * expected value. 165 */ 166 private class QueryNumEvents implements Command { 167 int expected; 168 169 public QueryNumEvents(int expected) { 170 this.expected = expected; 171 } 172 173 public void execute() { 174 Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); 175 assertEquals(expected, cursor.getCount()); 176 cursor.close(); 177 } 178 } 179 180 181 /** 182 * This command dumps the list of events to the log for debugging. 183 */ 184 private class DumpEvents implements Command { 185 186 public DumpEvents() { 187 } 188 189 public void execute() { 190 Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); 191 dumpCursor(cursor); 192 cursor.close(); 193 } 194 } 195 196 /** 197 * This command dumps the list of instances to the log for debugging. 198 */ 199 private class DumpInstances implements Command { 200 long begin; 201 long end; 202 203 public DumpInstances(String startDate, String endDate) { 204 Time time = new Time(DEFAULT_TIMEZONE); 205 time.parse3339(startDate); 206 begin = time.toMillis(false /* use isDst */); 207 time.parse3339(endDate); 208 end = time.toMillis(false /* use isDst */); 209 } 210 211 public void execute() { 212 Cursor cursor = queryInstances(begin, end); 213 dumpCursor(cursor); 214 cursor.close(); 215 } 216 } 217 218 /** 219 * This command queries the number of instances and compares it to the given 220 * expected value. 221 */ 222 private class QueryNumInstances implements Command { 223 int expected; 224 long begin; 225 long end; 226 227 public QueryNumInstances(String startDate, String endDate, int expected) { 228 Time time = new Time(DEFAULT_TIMEZONE); 229 time.parse3339(startDate); 230 begin = time.toMillis(false /* use isDst */); 231 time.parse3339(endDate); 232 end = time.toMillis(false /* use isDst */); 233 this.expected = expected; 234 } 235 236 public void execute() { 237 Cursor cursor = queryInstances(begin, end); 238 assertEquals(expected, cursor.getCount()); 239 cursor.close(); 240 } 241 } 242 243 /** 244 * When this command runs it verifies that all of the instances in the 245 * given range match the expected instances (each instance is specified by 246 * a start date). 247 * If you just want to verify that an instance exists in a given date 248 * range, use {@link VerifyInstance} instead. 249 */ 250 private class VerifyAllInstances implements Command { 251 long[] instances; 252 long begin; 253 long end; 254 255 public VerifyAllInstances(String startDate, String endDate, String[] dates) { 256 Time time = new Time(DEFAULT_TIMEZONE); 257 time.parse3339(startDate); 258 begin = time.toMillis(false /* use isDst */); 259 time.parse3339(endDate); 260 end = time.toMillis(false /* use isDst */); 261 262 if (dates == null) { 263 return; 264 } 265 266 // Convert all the instance date strings to UTC milliseconds 267 int len = dates.length; 268 this.instances = new long[len]; 269 int index = 0; 270 for (String instance : dates) { 271 time.parse3339(instance); 272 this.instances[index++] = time.toMillis(false /* use isDst */); 273 } 274 } 275 276 public void execute() { 277 Cursor cursor = queryInstances(begin, end); 278 int len = 0; 279 if (instances != null) { 280 len = instances.length; 281 } 282 if (len != cursor.getCount()) { 283 dumpCursor(cursor); 284 } 285 assertEquals("number of instances don't match", len, cursor.getCount()); 286 287 if (instances == null) { 288 return; 289 } 290 291 int beginColumn = cursor.getColumnIndex(Instances.BEGIN); 292 while (cursor.moveToNext()) { 293 long begin = cursor.getLong(beginColumn); 294 295 // Search the list of expected instances for a matching start 296 // time. 297 boolean found = false; 298 for (long instance : instances) { 299 if (instance == begin) { 300 found = true; 301 break; 302 } 303 } 304 if (!found) { 305 int titleColumn = cursor.getColumnIndex(Events.TITLE); 306 int allDayColumn = cursor.getColumnIndex(Events.ALL_DAY); 307 308 String title = cursor.getString(titleColumn); 309 boolean allDay = cursor.getInt(allDayColumn) != 0; 310 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | 311 DateUtils.FORMAT_24HOUR; 312 if (allDay) { 313 flags |= DateUtils.FORMAT_UTC; 314 } else { 315 flags |= DateUtils.FORMAT_SHOW_TIME; 316 } 317 String date = DateUtils.formatDateRange(mContext, begin, begin, flags); 318 String mesg = String.format("Test failed!" 319 + " unexpected instance (\"%s\") at %s", 320 title, date); 321 Log.e(TAG, mesg); 322 } 323 if (!found) { 324 dumpCursor(cursor); 325 } 326 assertTrue(found); 327 } 328 cursor.close(); 329 } 330 } 331 332 /** 333 * When this command runs it verifies that the given instance exists in 334 * the given date range. 335 */ 336 private class VerifyInstance implements Command { 337 long instance; 338 boolean allDay; 339 long begin; 340 long end; 341 342 /** 343 * Creates a command to check that the given range [startDate,endDate] 344 * contains a specific instance of an event (specified by "date"). 345 * 346 * @param startDate the beginning of the date range 347 * @param endDate the end of the date range 348 * @param date the date or date-time string of an event instance 349 */ 350 public VerifyInstance(String startDate, String endDate, String date) { 351 Time time = new Time(DEFAULT_TIMEZONE); 352 time.parse3339(startDate); 353 begin = time.toMillis(false /* use isDst */); 354 time.parse3339(endDate); 355 end = time.toMillis(false /* use isDst */); 356 357 // Convert the instance date string to UTC milliseconds 358 time.parse3339(date); 359 allDay = time.allDay; 360 instance = time.toMillis(false /* use isDst */); 361 } 362 363 public void execute() { 364 Cursor cursor = queryInstances(begin, end); 365 int beginColumn = cursor.getColumnIndex(Instances.BEGIN); 366 boolean found = false; 367 while (cursor.moveToNext()) { 368 long begin = cursor.getLong(beginColumn); 369 370 if (instance == begin) { 371 found = true; 372 break; 373 } 374 } 375 if (!found) { 376 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE; 377 if (allDay) { 378 flags |= DateUtils.FORMAT_UTC; 379 } else { 380 flags |= DateUtils.FORMAT_SHOW_TIME; 381 } 382 String date = DateUtils.formatDateRange(mContext, instance, instance, flags); 383 String mesg = String.format("Test failed!" 384 + " cannot find instance at %s", 385 date); 386 Log.e(TAG, mesg); 387 } 388 assertTrue(found); 389 cursor.close(); 390 } 391 } 392 393 /** 394 * This class stores all the useful information about an event. 395 */ 396 private class EventInfo { 397 String mTitle; 398 String mDescription; 399 String mTimezone; 400 boolean mAllDay; 401 long mDtstart; 402 long mDtend; 403 String mRrule; 404 String mDuration; 405 String mOriginalTitle; 406 long mOriginalInstance; 407 int mSyncId; 408 409 // Constructor for normal events, using the default timezone 410 public EventInfo(String title, String startDate, String endDate, 411 boolean allDay) { 412 init(title, startDate, endDate, allDay, DEFAULT_TIMEZONE); 413 } 414 415 // Constructor for normal events, specifying the timezone 416 public EventInfo(String title, String startDate, String endDate, 417 boolean allDay, String timezone) { 418 init(title, startDate, endDate, allDay, timezone); 419 } 420 421 public void init(String title, String startDate, String endDate, 422 boolean allDay, String timezone) { 423 mTitle = title; 424 Time time = new Time(); 425 if (allDay) { 426 time.timezone = Time.TIMEZONE_UTC; 427 } else if (timezone != null) { 428 time.timezone = timezone; 429 } 430 mTimezone = time.timezone; 431 time.parse3339(startDate); 432 mDtstart = time.toMillis(false /* use isDst */); 433 time.parse3339(endDate); 434 mDtend = time.toMillis(false /* use isDst */); 435 mDuration = null; 436 mRrule = null; 437 mAllDay = allDay; 438 } 439 440 // Constructor for repeating events, using the default timezone 441 public EventInfo(String title, String description, String startDate, String endDate, 442 String rrule, boolean allDay) { 443 init(title, description, startDate, endDate, rrule, allDay, DEFAULT_TIMEZONE); 444 } 445 446 // Constructor for repeating events, specifying the timezone 447 public EventInfo(String title, String description, String startDate, String endDate, 448 String rrule, boolean allDay, String timezone) { 449 init(title, description, startDate, endDate, rrule, allDay, timezone); 450 } 451 452 public void init(String title, String description, String startDate, String endDate, 453 String rrule, boolean allDay, String timezone) { 454 mTitle = title; 455 mDescription = description; 456 Time time = new Time(); 457 if (allDay) { 458 time.timezone = Time.TIMEZONE_UTC; 459 } else if (timezone != null) { 460 time.timezone = timezone; 461 } 462 mTimezone = time.timezone; 463 time.parse3339(startDate); 464 mDtstart = time.toMillis(false /* use isDst */); 465 if (endDate != null) { 466 time.parse3339(endDate); 467 mDtend = time.toMillis(false /* use isDst */); 468 } 469 if (allDay) { 470 long days = 1; 471 if (endDate != null) { 472 days = (mDtend - mDtstart) / DateUtils.DAY_IN_MILLIS; 473 } 474 mDuration = "P" + days + "D"; 475 } else { 476 long seconds = (mDtend - mDtstart) / DateUtils.SECOND_IN_MILLIS; 477 mDuration = "P" + seconds + "S"; 478 } 479 mRrule = rrule; 480 mAllDay = allDay; 481 } 482 483 // Constructor for recurrence exceptions, using the default timezone 484 public EventInfo(String originalTitle, String originalInstance, String title, 485 String description, String startDate, String endDate, boolean allDay) { 486 init(originalTitle, originalInstance, 487 title, description, startDate, endDate, allDay, DEFAULT_TIMEZONE); 488 } 489 490 public void init(String originalTitle, String originalInstance, 491 String title, String description, String startDate, String endDate, 492 boolean allDay, String timezone) { 493 mOriginalTitle = originalTitle; 494 Time time = new Time(timezone); 495 time.parse3339(originalInstance); 496 mOriginalInstance = time.toMillis(false /* use isDst */); 497 init(title, description, startDate, endDate, null /* rrule */, allDay, timezone); 498 } 499 } 500 501 private class InstanceInfo { 502 EventInfo mEvent; 503 long mBegin; 504 long mEnd; 505 int mExpectedOccurrences; 506 507 public InstanceInfo(String eventName, String startDate, String endDate, int expected) { 508 // Find the test index that contains the given event name 509 mEvent = findEvent(eventName); 510 Time time = new Time(mEvent.mTimezone); 511 time.parse3339(startDate); 512 mBegin = time.toMillis(false /* use isDst */); 513 time.parse3339(endDate); 514 mEnd = time.toMillis(false /* use isDst */); 515 mExpectedOccurrences = expected; 516 } 517 } 518 519 private class BusyBitInfo { 520 EventInfo[] mEvents; 521 int mStartDay; 522 int mNumDays; 523 int[] mBusyBits; 524 int[] mAllDayCounts; 525 526 public BusyBitInfo(EventInfo[] events, String startDate, int numDays, 527 int[] busybits, int[] allDayCounts) { 528 mEvents = events; 529 Time time = new Time(DEFAULT_TIMEZONE); 530 time.parse3339(startDate); 531 long millis = time.toMillis(true /* ignore isDst */); 532 mStartDay = Time.getJulianDay(millis, time.gmtoff); 533 mNumDays = numDays; 534 mBusyBits = busybits; 535 mAllDayCounts = allDayCounts; 536 } 537 } 538 539 /** 540 * This is the main table of events. The events in this table are 541 * referred to by name in other places. 542 */ 543 private EventInfo[] mEvents = { 544 new EventInfo("normal0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", false), 545 new EventInfo("normal1", "2008-05-26T08:30:00", "2008-05-26T09:30:00", false), 546 new EventInfo("normal2", "2008-05-26T14:30:00", "2008-05-26T15:30:00", false), 547 new EventInfo("allday0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", true), 548 new EventInfo("allday1", "2008-05-02T00:00:00", "2008-05-31T00:00:00", true), 549 new EventInfo("daily0", "daily from 5/1/2008 12am to 1am", 550 "2008-05-01T00:00:00", "2008-05-01T01:00:00", 551 "FREQ=DAILY;WKST=SU", false), 552 new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am", 553 "2008-05-01T08:30:00", "2008-05-01T09:30:00", 554 "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false), 555 new EventInfo("daily2", "daily from 5/1/2008 8:45am to 9:15am until 5/3/2008 10am", 556 "2008-05-01T08:45:00", "2008-05-01T09:15:00", 557 "FREQ=DAILY;UNTIL=20080503T170000Z;WKST=SU", false), 558 new EventInfo("allday daily0", "all-day daily from 5/1/2008", 559 "2008-05-01", null, 560 "FREQ=DAILY;WKST=SU", true), 561 new EventInfo("allday daily1", "all-day daily from 5/1/2008 until 5/3/2008", 562 "2008-05-01", null, 563 "FREQ=DAILY;UNTIL=20080503T000000Z;WKST=SU", true), 564 new EventInfo("allday weekly0", "all-day weekly from 5/1/2008", 565 "2008-05-01", null, 566 "FREQ=WEEKLY;WKST=SU", true), 567 new EventInfo("allday weekly1", "all-day for 2 days weekly from 5/1/2008", 568 "2008-05-01", "2008-05-03", 569 "FREQ=WEEKLY;WKST=SU", true), 570 new EventInfo("allday yearly0", "all-day yearly on 5/1/2008", 571 "2008-05-01T", null, 572 "FREQ=YEARLY;WKST=SU", true), 573 new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm", 574 "2008-05-06T13:00:00", "2008-05-06T14:00:00", 575 "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false), 576 new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 2:30pm to 3:30pm", 577 "2008-05-06T14:30:00", "2008-05-06T15:30:00", 578 "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false), 579 new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm", 580 "2008-05-20T15:00:00", "2008-05-20T16:00:00", 581 "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false), 582 new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 12:00am to 12:10am", 583 "2008-05-01T00:00:00", "2008-05-01T00:10:00", 584 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false), 585 new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight", 586 "2008-05-31T23:00:00", "2008-06-01T00:00:00", 587 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false), 588 new EventInfo("daily0", "2008-05-01T00:00:00", 589 "except0", "daily0 exception for 5/1/2008 12am, change to 5/1/2008 2am to 3am", 590 "2008-05-01T02:00:00", "2008-05-01T01:03:00", false), 591 new EventInfo("daily0", "2008-05-03T00:00:00", 592 "except1", "daily0 exception for 5/3/2008 12am, change to 5/3/2008 2am to 3am", 593 "2008-05-03T02:00:00", "2008-05-03T01:03:00", false), 594 new EventInfo("daily0", "2008-05-02T00:00:00", 595 "except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008", 596 "2008-01-02T00:00:00", "2008-01-02T01:00:00", false), 597 new EventInfo("weekly0", "2008-05-13T13:00:00", 598 "except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm", 599 "2008-12-11T13:00:00", "2008-12-11T14:00:00", false), 600 new EventInfo("weekly0", "2008-05-13T13:00:00", 601 "cancel0", "weekly0 exception for 5/13/2008 1pm", 602 "2008-05-13T13:00:00", "2008-05-13T14:00:00", false), 603 new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm", 604 "2008-05-01T13:00:00", "2008-05-01T14:00:00", 605 "FREQ=YEARLY;WKST=SU", false), 606 }; 607 608 /** 609 * This table is used to create repeating events and then check that the 610 * number of instances within a given range matches the expected number 611 * of instances. 612 */ 613 private InstanceInfo[] mInstanceRanges = { 614 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T00:01:00", 1), 615 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1), 616 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 2), 617 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T23:59:00", 2), 618 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T00:01:00", 1), 619 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T01:00:00", 1), 620 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", 2), 621 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 31), 622 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-06-01T23:59:00", 32), 623 624 new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1), 625 new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 2), 626 627 new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1), 628 new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 3), 629 630 new InstanceInfo("allday daily0", "2008-05-01", "2008-05-07", 7), 631 new InstanceInfo("allday daily1", "2008-05-01", "2008-05-07", 3), 632 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-07", 1), 633 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-08", 2), 634 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-31", 5), 635 new InstanceInfo("allday weekly1", "2008-05-01", "2008-05-31", 5), 636 new InstanceInfo("allday yearly0", "2008-05-01", "2009-04-30", 1), 637 new InstanceInfo("allday yearly0", "2008-05-01", "2009-05-02", 2), 638 639 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0), 640 new InstanceInfo("weekly0", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1), 641 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 4), 642 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 8), 643 644 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0), 645 new InstanceInfo("weekly1", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1), 646 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 2), 647 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 4), 648 649 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T13:00:00", 0), 650 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T15:00:00", 1), 651 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-05-31T00:00:00", 0), 652 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T14:59:00", 0), 653 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T15:00:00", 1), 654 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1), 655 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 2), 656 657 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1), 658 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1), 659 new InstanceInfo("monthly1", "2008-05-01T00:10:00", "2008-05-31T23:59:00", 1), 660 new InstanceInfo("monthly1", "2008-05-01T00:11:00", "2008-05-31T23:59:00", 0), 661 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-06-01T00:00:00", 2), 662 663 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 0), 664 new InstanceInfo("monthly2", "2008-05-01T00:10:00", "2008-05-31T23:00:00", 1), 665 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-07-01T00:00:00", 1), 666 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-08-01T00:00:00", 2), 667 668 new InstanceInfo("yearly0", "2008-05-01", "2009-04-30", 1), 669 new InstanceInfo("yearly0", "2008-05-01", "2009-05-02", 2), 670 }; 671 672 /** 673 * This tables of events is used to test the BusyBit database table. 674 */ 675 private EventInfo[] mBusyBitEvents = { 676 new EventInfo("1: 12am - 1am", "2008-05-01T00:00:00", "2008-05-01T01:00:00", false), 677 new EventInfo("2: 1:30am - 2am", "2008-05-02T01:30:00", "2008-05-02T02:00:00", false), 678 new EventInfo("3: 3am - 5am", "2008-05-03T03:00:00", "2008-05-03T05:00:00", false), 679 new EventInfo("4: 12am - 5am", "2008-05-04T00:00:00", "2008-05-04T05:00:00", false), 680 new EventInfo("5: 1am - 2am", "2008-05-05T01:00:00", "2008-05-05T02:00:00", false), 681 new EventInfo("5: 8am - 9am", "2008-05-05T08:00:00", "2008-05-05T09:00:00", false), 682 new EventInfo("6: 1am - 10am", "2008-05-06T01:00:00", "2008-05-06T10:00:00", false), 683 new EventInfo("6: 8am - 9am", "2008-05-06T08:00:00", "2008-05-06T09:00:00", false), 684 new EventInfo("7: 1am - 5am", "2008-05-07T01:00:00", "2008-05-07T05:00:00", false), 685 new EventInfo("7: 12am - 2am", "2008-05-07T00:00:00", "2008-05-07T02:00:00", false), 686 new EventInfo("7: 8am - 9am", "2008-05-07T08:00:00", "2008-05-07T09:00:00", false), 687 new EventInfo("7: 1pm - 2pm", "2008-05-07T13:00:00", "2008-05-07T14:00:00", false), 688 new EventInfo("7: 3:30pm - 4:30pm", "2008-05-07T15:30:00", "2008-05-07T16:30:00", 689 false), 690 new EventInfo("7: 7pm - 8pm", "2008-05-07T19:00:00", "2008-05-07T20:00:00", false), 691 new EventInfo("7: 6:30pm - 7:30pm", "2008-05-07T18:30:00", "2008-05-07T19:30:00", 692 false), 693 new EventInfo("7: 11pm - midnight", "2008-05-07T23:00:00", "2008-05-08T00:00:00", 694 false), 695 new EventInfo("8: 1am - 2am", "2008-05-08T01:00:00", "2008-05-08T02:00:00", false), 696 new EventInfo("8: 3am - 4am", "2008-05-08T03:00:00", "2008-05-08T04:00:00", false), 697 new EventInfo("8: 5am - 6am", "2008-05-08T05:00:00", "2008-05-08T06:00:00", false), 698 new EventInfo("8: 7am - 8am", "2008-05-08T07:00:00", "2008-05-08T08:00:00", false), 699 new EventInfo("8: 9am - 10am", "2008-05-08T09:00:00", "2008-05-08T10:00:00", false), 700 new EventInfo("8: 11am - 12pm", "2008-05-08T11:00:00", "2008-05-08T12:00:00", false), 701 new EventInfo("8: 1pm - 2pm", "2008-05-08T13:00:00", "2008-05-08T14:00:00", false), 702 new EventInfo("8: 3pm - 4pm", "2008-05-08T15:00:00", "2008-05-08T16:00:00", false), 703 new EventInfo("8: 5pm - 6pm", "2008-05-08T17:00:00", "2008-05-08T18:00:00", false), 704 new EventInfo("8: 7pm - 8pm", "2008-05-08T19:00:00", "2008-05-08T20:00:00", false), 705 new EventInfo("8: 9pm - 10pm", "2008-05-08T21:00:00", "2008-05-08T22:00:00", false), 706 new EventInfo("8: 11pm - midnight", "2008-05-08T23:00:00", "2008-05-09T00:00:00", 707 false), 708 new EventInfo("10: 12am - midnight", "2008-05-10T00:00:00", "2008-05-11T00:00:00", 709 false), 710 new EventInfo("12: 1 day", "2008-05-12T00:00:00", "2008-05-13T00:00:00", true), 711 new EventInfo("14: 1 day", "2008-05-14T00:00:00", "2008-05-15T00:00:00", true), 712 new EventInfo("14: 2 days", "2008-05-14T00:00:00", "2008-05-16T00:00:00", true), 713 new EventInfo("14: 3 days", "2008-05-14T00:00:00", "2008-05-17T00:00:00", true), 714 new EventInfo("15: 1am - 2am", "2008-05-15T01:00:00", "2008-05-15T02:00:00", false), 715 new EventInfo("16: 10am - 11am", "2008-05-16T10:00:00", "2008-05-16T11:00:00", false), 716 new EventInfo("16: 11pm - midnight", "2008-05-16T23:00:00", "2008-05-17T00:00:00", 717 false), 718 }; 719 720 private EventInfo[] mBusyBitRepeatingEvents = { 721 new EventInfo("daily0", "daily from 5/1/2008 12am to 1am", 722 "2008-05-01T00:00:00", "2008-05-01T01:00:00", 723 "FREQ=DAILY;WKST=SU", false), 724 new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am", 725 "2008-05-01T08:30:00", "2008-05-01T09:30:00", 726 "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false), 727 new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm", 728 "2008-05-06T13:00:00", "2008-05-06T14:00:00", 729 "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false), 730 new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 4:30am to 5:30am", 731 "2008-05-06T04:30:00", "2008-05-06T05:30:00", 732 "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false), 733 new EventInfo("weekly2", "weekly from 5/5/2008 on Mon 1 day", 734 "2008-05-05T00:00:00", "2008-05-06T00:00:00", 735 "FREQ=WEEKLY;BYDAY=MO;WKST=MO", true), 736 new EventInfo("weekly3", "weekly from 5/7/2008 on Wed 3 days", 737 "2008-05-07T00:00:00", "2008-05-10T00:00:00", 738 "FREQ=WEEKLY;BYDAY=WE;WKST=SU", true), 739 new EventInfo("weekly4", "weekly from 5/8/2008 on Thu 3 days", 740 "2008-05-08T00:00:00", "2008-05-11T00:00:00", 741 "FREQ=WEEKLY;BYDAY=TH;WKST=SU", true), 742 new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm", 743 "2008-05-20T15:00:00", "2008-05-20T16:00:00", 744 "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false), 745 new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 11:00am to 11:10am", 746 "2008-05-01T11:00:00", "2008-05-01T11:10:00", 747 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false), 748 new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight", 749 "2008-05-31T23:00:00", "2008-06-01T00:00:00", 750 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false), 751 }; 752 753 private BusyBitInfo[] mBusyBitTests = { 754 new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 1, 755 new int[] { 0x1 }, new int[] { 0 } ), 756 new BusyBitInfo(mBusyBitEvents, "2008-05-02T00:00:00", 1, 757 new int[] { 0x2 }, new int[] { 0 } ), 758 new BusyBitInfo(mBusyBitEvents, "2008-05-02T00:00:00", 2, 759 new int[] { 0x2, 0x18 }, new int[] { 0, 0 } ), 760 new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 3, 761 new int[] { 0x1, 0x2, 0x18 }, new int[] { 0, 0, 0 } ), 762 new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 8, 763 new int[] { 0x1, 0x2, 0x18, 0x1f, 0x102, 0x3fe, 0x8da11f, 0xaaaaaa }, 764 new int[] { 0, 0, 0, 0, 0, 0, 0, 0 } ), 765 new BusyBitInfo(mBusyBitEvents, "2008-05-10T00:00:00", 4, 766 new int[] { 0xffffff, 0x0, 0x0, 0x0 }, new int[] { 0, 0, 1, 0 } ), 767 new BusyBitInfo(mBusyBitEvents, "2008-05-14T00:00:00", 4, 768 new int[] { 0x0, 0x2, 0x800400, 0x0 }, new int[] { 3, 2, 1, 0 } ), 769 770 // Repeating events 771 new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-01T00:00:00", 3, 772 new int[] { 0xb01, 0x301, 0x1 }, new int[] { 0, 0, 0 } ), 773 new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-01T00:00:00", 10, 774 new int[] { 0xb01, 0x301, 0x1, 0x1, 0x1, 0x2031, 0x1, 0x1, 0x1, 0x1 }, 775 new int[] { 0, 0, 0, 0, 1, 0, 1, 2, 2, 1 } ), 776 new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-18T00:00:00", 11, 777 new int[] { 0x1, 0x1, 0xa031, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x2001, 0x1 }, 778 new int[] { 0, 1, 0, 1, 2, 2, 1, 0, 1, 0, 1 } ), 779 new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-30T00:00:00", 5, 780 new int[] { 0x1, 0x800001, 0x801, 0x1, 0x2031 }, 781 new int[] { 2, 1, 0, 1, 0 } ), 782 }; 783 784 /** 785 * This sequence of commands inserts and deletes some events. 786 */ 787 private Command[] mNormalInsertDelete = { 788 new Insert("normal0"), 789 new Insert("normal1"), 790 new Insert("normal2"), 791 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 3), 792 new Delete("normal1", 1), 793 new QueryNumEvents(2), 794 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 2), 795 new Delete("normal1", 0), 796 new Delete("normal2", 1), 797 new QueryNumEvents(1), 798 new Delete("normal0", 1), 799 new QueryNumEvents(0), 800 }; 801 802 /** 803 * This sequence of commands inserts and deletes some all-day events. 804 */ 805 private Command[] mAlldayInsertDelete = { 806 new Insert("allday0"), 807 new Insert("allday1"), 808 new QueryNumEvents(2), 809 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-01T00:01:00", 0), 810 new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 2), 811 new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1), 812 new Delete("allday0", 1), 813 new QueryNumEvents(1), 814 new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 1), 815 new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1), 816 new Delete("allday1", 1), 817 new QueryNumEvents(0), 818 }; 819 820 /** 821 * This sequence of commands inserts and deletes some repeating events. 822 */ 823 private Command[] mRecurringInsertDelete = { 824 new Insert("daily0"), 825 new Insert("daily1"), 826 new QueryNumEvents(2), 827 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 3), 828 new QueryNumInstances("2008-05-01T01:01:00", "2008-05-02T00:01:00", 2), 829 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 6), 830 new Delete("daily1", 1), 831 new QueryNumEvents(1), 832 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 2), 833 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 4), 834 new Delete("daily0", 1), 835 new QueryNumEvents(0), 836 }; 837 838 /** 839 * This sequence of commands creates a recurring event with a recurrence 840 * exception that moves an event outside the expansion window. It checks that the 841 * recurrence exception does not occur in the Instances database table. 842 * Bug 1642665 843 */ 844 private Command[] mExceptionWithMovedRecurrence = { 845 new Insert("daily0"), 846 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 847 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 848 "2008-05-03T00:00:00", }), 849 new Insert("except2"), 850 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 851 new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}), 852 }; 853 854 /** 855 * This sequence of commands deletes (cancels) one instance of a recurrence. 856 */ 857 private Command[] mCancelInstance = { 858 new Insert("weekly0"), 859 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00", 860 new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00", 861 "2008-05-20T13:00:00", }), 862 new Insert("cancel0"), 863 new Update("cancel0", new KeyValue[] { 864 new KeyValue(Calendar.EventsColumns.STATUS, 865 "" + Calendar.EventsColumns.STATUS_CANCELED), 866 }), 867 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00", 868 new String[] {"2008-05-06T13:00:00", 869 "2008-05-20T13:00:00", }), 870 }; 871 /** 872 * This sequence of commands creates a recurring event with a recurrence 873 * exception that moves an event from outside the expansion window into the 874 * expansion window. 875 */ 876 private Command[] mExceptionWithMovedRecurrence2 = { 877 new Insert("weekly0"), 878 new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00", 879 new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00", 880 "2008-12-16T13:00:00", }), 881 new Insert("except3"), 882 new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00", 883 new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00", 884 "2008-12-11T13:00:00", "2008-12-16T13:00:00", }), 885 }; 886 /** 887 * This sequence of commands creates a recurring event with a recurrence 888 * exception and then changes the end time of the recurring event. It then 889 * checks that the recurrence exception does not occur in the Instances 890 * database table. 891 */ 892 private Command[] 893 mExceptionWithTruncatedRecurrence = { 894 new Insert("daily0"), 895 // Verify 4 occurrences of the "daily0" repeating event 896 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 897 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 898 "2008-05-03T00:00:00", "2008-05-04T00:00:00"}), 899 new Insert("except1"), 900 new QueryNumEvents(2), 901 902 // Verify that one of the 4 occurrences has its start time changed 903 // so that it now matches the recurrence exception. 904 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 905 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 906 "2008-05-03T02:00:00", "2008-05-04T00:00:00"}), 907 908 // Change the end time of "daily0" but it still includes the 909 // recurrence exception. 910 new Update("daily0", new KeyValue[] { 911 new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080505T150000Z;WKST=SU"), 912 }), 913 914 // Verify that the recurrence exception is still there 915 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 916 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 917 "2008-05-03T02:00:00", "2008-05-04T00:00:00"}), 918 // This time change the end time of "daily0" so that it excludes 919 // the recurrence exception. 920 new Update("daily0", new KeyValue[] { 921 new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"), 922 }), 923 // The server will cancel the out-of-range exception. 924 // It would be nice for the provider to handle this automatically, 925 // but for now simulate the server-side cancel. 926 new Update("except1", new KeyValue[] { 927 new KeyValue(Calendar.EventsColumns.STATUS, "" + Calendar.EventsColumns.STATUS_CANCELED), 928 }), 929 // Verify that the recurrence exception does not appear. 930 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 931 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}), 932 }; 933 934 /** 935 * Bug 135848. Ensure that a recurrence exception is displayed even if the recurrence 936 * is not present. 937 */ 938 private Command[] mExceptionWithNoRecurrence = { 939 new Insert("except0"), 940 new QueryNumEvents(1), 941 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 942 new String[] {"2008-05-01T02:00:00"}), 943 }; 944 945 private EventInfo findEvent(String name) { 946 int len = mEvents.length; 947 for (int ii = 0; ii < len; ii++) { 948 EventInfo event = mEvents[ii]; 949 if (name.equals(event.mTitle)) { 950 return event; 951 } 952 } 953 return null; 954 } 955 956 public CalendarProvider2Test() { 957 super(CalendarProvider2ForTesting.class, Calendar.AUTHORITY); 958 } 959 960 @Override 961 protected void setUp() throws Exception { 962 super.setUp(); 963 964 mContext = getMockContext(); 965 mResolver = getMockContentResolver(); 966 mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds")); 967 mResolver.addProvider("sync", new MockProvider("sync")); 968 969 CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); 970 helper.wipeData(); 971 mDb = helper.getWritableDatabase(); 972 mMetaData = getProvider().mMetaData; 973 } 974 975 @Override 976 protected void tearDown() throws Exception { 977 mDb.close(); 978 mDb = null; 979 getProvider().getDatabaseHelper().close(); 980 super.tearDown(); 981 } 982 983 /** 984 * Dumps the contents of the given cursor to the log. For debugging. 985 * @param cursor the database cursor 986 */ 987 private void dumpCursor(Cursor cursor) { 988 cursor.moveToPosition(-1); 989 String[] cols = cursor.getColumnNames(); 990 991 Log.i(TAG, "dumpCursor() count: " + cursor.getCount()); 992 int index = 0; 993 while (cursor.moveToNext()) { 994 Log.i(TAG, index + " {"); 995 for (int i = 0; i < cols.length; i++) { 996 Log.i(TAG, " " + cols[i] + '=' + cursor.getString(i)); 997 } 998 Log.i(TAG, "}"); 999 index += 1; 1000 } 1001 cursor.moveToPosition(-1); 1002 } 1003 1004 private int insertCal(String name, String timezone) { 1005 return insertCal(name, timezone, "joe@joe.com"); 1006 } 1007 1008 private int insertCal(String name, String timezone, String account) { 1009 ContentValues m = new ContentValues(); 1010 m.put(Calendars.NAME, name); 1011 m.put(Calendars.DISPLAY_NAME, name); 1012 m.put(Calendars.COLOR, "0xff123456"); 1013 m.put(Calendars.TIMEZONE, timezone); 1014 m.put(Calendars.SELECTED, 1); 1015 m.put(Calendars.URL, CALENDAR_URL); 1016 m.put(Calendars.OWNER_ACCOUNT, "joe@joe.com"); 1017 m.put(Calendars._SYNC_ACCOUNT, account); 1018 m.put(Calendars._SYNC_ACCOUNT_TYPE, "com.google"); 1019 1020 Uri url = mResolver.insert(Uri.parse("content://calendar/calendars"), m); 1021 String id = url.getLastPathSegment(); 1022 return Integer.parseInt(id); 1023 } 1024 1025 private Uri insertEvent(int calId, EventInfo event) { 1026 return insertEvent(calId, event, "joe@joe.com"); 1027 } 1028 1029 private Uri insertEvent(int calId, EventInfo event, String account) { 1030 if (mWipe) { 1031 // Wipe instance table so it will be regenerated 1032 mMetaData.clearInstanceRange(); 1033 } 1034 ContentValues m = new ContentValues(); 1035 m.put(Events.CALENDAR_ID, calId); 1036 m.put(Events.TITLE, event.mTitle); 1037 m.put(Events.DTSTART, event.mDtstart); 1038 m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0); 1039 m.put(Calendars._SYNC_ACCOUNT, account); 1040 m.put(Calendars._SYNC_ACCOUNT_TYPE, "com.google"); 1041 1042 if (event.mRrule == null) { 1043 // This is a normal event 1044 m.put(Events.DTEND, event.mDtend); 1045 } else { 1046 // This is a repeating event 1047 m.put(Events.RRULE, event.mRrule); 1048 m.put(Events.DURATION, event.mDuration); 1049 } 1050 1051 if (event.mDescription != null) { 1052 m.put(Events.DESCRIPTION, event.mDescription); 1053 } 1054 if (event.mTimezone != null) { 1055 m.put(Events.EVENT_TIMEZONE, event.mTimezone); 1056 } 1057 1058 if (event.mOriginalTitle != null) { 1059 // This is a recurrence exception. 1060 EventInfo recur = findEvent(event.mOriginalTitle); 1061 assertNotNull(recur); 1062 String syncId = String.format("%d", recur.mSyncId); 1063 m.put(Events.ORIGINAL_EVENT, syncId); 1064 m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0); 1065 m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance); 1066 } 1067 Uri url = mResolver.insert(mEventsUri, m); 1068 1069 // Create a fake _sync_id and add it to the event. Update the database 1070 // directly so that we don't trigger any validation checks in the 1071 // CalendarProvider. 1072 long id = ContentUris.parseId(url); 1073 mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id); 1074 event.mSyncId = mGlobalSyncId; 1075 mGlobalSyncId += 1; 1076 1077 return url; 1078 } 1079 1080 /** 1081 * Deletes all the events that match the given title. 1082 * @param title the given title to match events on 1083 * @return the number of rows deleted 1084 */ 1085 private int deleteMatchingEvents(String title) { 1086 Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID }, 1087 "title=?", new String[] { title }, null); 1088 int numRows = 0; 1089 while (cursor.moveToNext()) { 1090 long id = cursor.getLong(0); 1091 // Do delete as a sync adapter so event is really deleted, not just marked 1092 // as deleted. 1093 Uri uri = updatedUri(ContentUris.withAppendedId(Events.CONTENT_URI, id), true); 1094 numRows += mResolver.delete(uri, null, null); 1095 } 1096 cursor.close(); 1097 return numRows; 1098 } 1099 1100 /** 1101 * Updates all the events that match the given title. 1102 * @param title the given title to match events on 1103 * @return the number of rows updated 1104 */ 1105 private int updateMatchingEvents(String title, ContentValues values) { 1106 String[] projection = new String[] { 1107 Events._ID, 1108 Events.DTSTART, 1109 Events.DTEND, 1110 Events.DURATION, 1111 Events.ALL_DAY, 1112 Events.RRULE, 1113 Events.EVENT_TIMEZONE, 1114 Events.ORIGINAL_EVENT, 1115 }; 1116 Cursor cursor = mResolver.query(mEventsUri, projection, 1117 "title=?", new String[] { title }, null); 1118 int numRows = 0; 1119 while (cursor.moveToNext()) { 1120 long id = cursor.getLong(0); 1121 1122 // If any of the following fields are being changed, then we need 1123 // to include all of them. 1124 if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND) 1125 || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY) 1126 || values.containsKey(Events.RRULE) 1127 || values.containsKey(Events.EVENT_TIMEZONE) 1128 || values.containsKey(Calendar.EventsColumns.STATUS)) { 1129 long dtstart = cursor.getLong(1); 1130 long dtend = cursor.getLong(2); 1131 String duration = cursor.getString(3); 1132 boolean allDay = cursor.getInt(4) != 0; 1133 String rrule = cursor.getString(5); 1134 String timezone = cursor.getString(6); 1135 String originalEvent = cursor.getString(7); 1136 1137 if (!values.containsKey(Events.DTSTART)) { 1138 values.put(Events.DTSTART, dtstart); 1139 } 1140 // Don't add DTEND for repeating events 1141 if (!values.containsKey(Events.DTEND) && rrule == null) { 1142 values.put(Events.DTEND, dtend); 1143 } 1144 if (!values.containsKey(Events.DURATION) && duration != null) { 1145 values.put(Events.DURATION, duration); 1146 } 1147 if (!values.containsKey(Events.ALL_DAY)) { 1148 values.put(Events.ALL_DAY, allDay ? 1 : 0); 1149 } 1150 if (!values.containsKey(Events.RRULE) && rrule != null) { 1151 values.put(Events.RRULE, rrule); 1152 } 1153 if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) { 1154 values.put(Events.EVENT_TIMEZONE, timezone); 1155 } 1156 if (!values.containsKey(Events.ORIGINAL_EVENT) && originalEvent != null) { 1157 values.put(Events.ORIGINAL_EVENT, originalEvent); 1158 } 1159 } 1160 1161 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id); 1162 numRows += mResolver.update(uri, values, null, null); 1163 } 1164 cursor.close(); 1165 return numRows; 1166 } 1167 1168 private void deleteAllEvents() { 1169 mDb.execSQL("DELETE FROM Events;"); 1170 mMetaData.clearInstanceRange(); 1171 } 1172 1173 public void testInsertNormalEvents() throws Exception { 1174 Cursor cursor; 1175 Uri url = null; 1176 1177 int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1178 1179 cursor = mResolver.query(mEventsUri, null, null, null, null); 1180 assertEquals(0, cursor.getCount()); 1181 cursor.close(); 1182 1183 // Keep track of the number of normal events 1184 int numEvents = 0; 1185 1186 // "begin" is the earliest start time of all the normal events, 1187 // and "end" is the latest end time of all the normal events. 1188 long begin = 0, end = 0; 1189 1190 int len = mEvents.length; 1191 for (int ii = 0; ii < len; ii++) { 1192 EventInfo event = mEvents[ii]; 1193 // Skip repeating events and recurrence exceptions 1194 if (event.mRrule != null || event.mOriginalTitle != null) { 1195 continue; 1196 } 1197 if (numEvents == 0) { 1198 begin = event.mDtstart; 1199 end = event.mDtend; 1200 } else { 1201 if (begin > event.mDtstart) { 1202 begin = event.mDtstart; 1203 } 1204 if (end < event.mDtend) { 1205 end = event.mDtend; 1206 } 1207 } 1208 url = insertEvent(calId, event); 1209 numEvents += 1; 1210 } 1211 1212 // query one 1213 cursor = mResolver.query(url, null, null, null, null); 1214 assertEquals(1, cursor.getCount()); 1215 cursor.close(); 1216 1217 // query all 1218 cursor = mResolver.query(mEventsUri, null, null, null, null); 1219 assertEquals(numEvents, cursor.getCount()); 1220 cursor.close(); 1221 1222 // Check that the Instances table has one instance of each of the 1223 // normal events. 1224 cursor = queryInstances(begin, end); 1225 assertEquals(numEvents, cursor.getCount()); 1226 cursor.close(); 1227 } 1228 1229 public void testInsertRepeatingEvents() throws Exception { 1230 Cursor cursor; 1231 Uri url = null; 1232 1233 int calId = insertCal("Calendar0", "America/Los_Angeles"); 1234 1235 cursor = mResolver.query(mEventsUri, null, null, null, null); 1236 assertEquals(0, cursor.getCount()); 1237 cursor.close(); 1238 1239 // Keep track of the number of repeating events 1240 int numEvents = 0; 1241 1242 int len = mEvents.length; 1243 for (int ii = 0; ii < len; ii++) { 1244 EventInfo event = mEvents[ii]; 1245 // Skip normal events 1246 if (event.mRrule == null) { 1247 continue; 1248 } 1249 url = insertEvent(calId, event); 1250 numEvents += 1; 1251 } 1252 1253 // query one 1254 cursor = mResolver.query(url, null, null, null, null); 1255 assertEquals(1, cursor.getCount()); 1256 cursor.close(); 1257 1258 // query all 1259 cursor = mResolver.query(mEventsUri, null, null, null, null); 1260 assertEquals(numEvents, cursor.getCount()); 1261 cursor.close(); 1262 } 1263 1264 public void testInstanceRange() throws Exception { 1265 Cursor cursor; 1266 Uri url = null; 1267 1268 int calId = insertCal("Calendar0", "America/Los_Angeles"); 1269 1270 cursor = mResolver.query(mEventsUri, null, null, null, null); 1271 assertEquals(0, cursor.getCount()); 1272 cursor.close(); 1273 1274 int len = mInstanceRanges.length; 1275 for (int ii = 0; ii < len; ii++) { 1276 InstanceInfo instance = mInstanceRanges[ii]; 1277 EventInfo event = instance.mEvent; 1278 url = insertEvent(calId, event); 1279 cursor = queryInstances(instance.mBegin, instance.mEnd); 1280 if (instance.mExpectedOccurrences != cursor.getCount()) { 1281 Log.e(TAG, "Test failed! Instance index: " + ii); 1282 Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription 1283 + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]" 1284 + " expected: " + instance.mExpectedOccurrences); 1285 dumpCursor(cursor); 1286 } 1287 assertEquals(instance.mExpectedOccurrences, cursor.getCount()); 1288 cursor.close(); 1289 // Delete as sync_adapter so event is really deleted. 1290 int rows = mResolver.delete(updatedUri(url, true), 1291 null /* selection */, null /* selection args */); 1292 assertEquals(1, rows); 1293 } 1294 } 1295 1296 public void testBusyBitRange() throws Exception { 1297 Cursor cursor; 1298 Uri url = null; 1299 1300 int calId = insertCal("Calendar0", "America/Los_Angeles"); 1301 1302 cursor = mResolver.query(mEventsUri, null, null, null, null); 1303 dumpCursor(cursor); 1304 assertEquals(0, cursor.getCount()); 1305 cursor.close(); 1306 1307 int len = mBusyBitTests.length; 1308 for (int ii = 0; ii < len; ii++) { 1309 deleteAllEvents(); 1310 BusyBitInfo busyInfo = mBusyBitTests[ii]; 1311 EventInfo[] events = busyInfo.mEvents; 1312 int numEvents = events.length; 1313 for (int jj = 0; jj < numEvents; jj++) { 1314 EventInfo event = events[jj]; 1315 insertEvent(calId, event); 1316 } 1317 1318 int startDay = busyInfo.mStartDay; 1319 int numDays = busyInfo.mNumDays; 1320 int[] busybits = new int[numDays]; 1321 int[] allDayCounts = new int[numDays]; 1322 1323 if (false) { 1324 cursor = mResolver.query(mEventsUri, null, null, null, null); 1325 Log.i(TAG, "Dump of Events table, count: " + cursor.getCount()); 1326 dumpCursor(cursor); 1327 cursor.close(); 1328 1329 Time time = new Time(); 1330 time.setJulianDay(startDay); 1331 long begin = time.toMillis(true); 1332 int endDay = startDay + numDays - 1; 1333 time.setJulianDay(endDay); 1334 long end = time.toMillis(true); 1335 cursor = queryInstances(begin, end); 1336 Log.i(TAG, "Dump of Instances table, count: " + cursor.getCount() 1337 + " startDay: " + startDay + " endDay: " + endDay 1338 + " begin: " + begin + " end: " + end); 1339 dumpCursor(cursor); 1340 cursor.close(); 1341 } 1342 1343 cursor = queryBusyBits(startDay, numDays); 1344 try { 1345 int dayColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.DAY); 1346 int busybitColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.BUSYBITS); 1347 int allDayCountColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.ALL_DAY_COUNT); 1348 1349 while (cursor.moveToNext()) { 1350 int day = cursor.getInt(dayColumnIndex); 1351 int dayIndex = day - startDay; 1352 busybits[dayIndex] = cursor.getInt(busybitColumnIndex); 1353 allDayCounts[dayIndex] = cursor.getInt(allDayCountColumnIndex); 1354 } 1355 } finally { 1356 if (cursor != null) { 1357 cursor.close(); 1358 } 1359 } 1360 1361 // Compare the database busy bits with the expected busy bits 1362 for (int dayIndex = 0; dayIndex < numDays; dayIndex++) { 1363 if (busyInfo.mBusyBits[dayIndex] != busybits[dayIndex]) { 1364 String mesg = String.format("Test failed!" 1365 + " BusyBit test index: %d" 1366 + " day index: %d" 1367 + " mStartDay: %d mNumDays: %d" 1368 + " expected busybits: 0x%x was: 0x%x", 1369 ii, dayIndex, busyInfo.mStartDay, busyInfo.mNumDays, 1370 busyInfo.mBusyBits[dayIndex], busybits[dayIndex]); 1371 Log.e(TAG, mesg); 1372 1373 cursor = mResolver.query(mEventsUri, null, null, null, null); 1374 Log.i(TAG, "Dump of Events table, count: " + cursor.getCount()); 1375 dumpCursor(cursor); 1376 cursor.close(); 1377 } 1378 assertEquals(busyInfo.mBusyBits[dayIndex], busybits[dayIndex]); 1379 } 1380 1381 // Compare the database all-day counts with the expected all-day counts 1382 for (int dayIndex = 0; dayIndex < numDays; dayIndex++) { 1383 if (busyInfo.mAllDayCounts[dayIndex] != allDayCounts[dayIndex]) { 1384 String mesg = String.format("Test failed!" 1385 + " BusyBit test index: %d" 1386 + " day index: %d" 1387 + " expected all-day count: %d was: %d", 1388 ii, dayIndex, 1389 busyInfo.mAllDayCounts[dayIndex], allDayCounts[dayIndex]); 1390 Log.e(TAG, mesg); 1391 } 1392 assertEquals(busyInfo.mAllDayCounts[dayIndex], allDayCounts[dayIndex]); 1393 } 1394 } 1395 } 1396 1397 public void testEntityQuery() throws Exception { 1398 testInsertNormalEvents(); // To initialize 1399 1400 ContentValues reminder = new ContentValues(); 1401 reminder.put(Calendar.Reminders.EVENT_ID, 1); 1402 reminder.put(Calendar.Reminders.MINUTES, 10); 1403 reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_SMS); 1404 mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder); 1405 reminder.put(Calendar.Reminders.MINUTES, 20); 1406 mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder); 1407 1408 ContentValues extended = new ContentValues(); 1409 extended.put(Calendar.ExtendedProperties.NAME, "foo"); 1410 extended.put(Calendar.ExtendedProperties.VALUE, "bar"); 1411 extended.put(Calendar.ExtendedProperties.EVENT_ID, 2); 1412 mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended); 1413 extended.put(Calendar.ExtendedProperties.EVENT_ID, 1); 1414 mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended); 1415 extended.put(Calendar.ExtendedProperties.NAME, "foo2"); 1416 extended.put(Calendar.ExtendedProperties.VALUE, "bar2"); 1417 mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended); 1418 1419 ContentValues attendee = new ContentValues(); 1420 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe"); 1421 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com"); 1422 attendee.put(Calendar.Attendees.ATTENDEE_STATUS, 1423 Calendar.Attendees.ATTENDEE_STATUS_DECLINED); 1424 attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED); 1425 attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, 1426 Calendar.Attendees.RELATIONSHIP_PERFORMER); 1427 attendee.put(Calendar.Attendees.EVENT_ID, 3); 1428 mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee); 1429 1430 EntityIterator ei = EventsEntity.newEntityIterator( 1431 mResolver.query(EventsEntity.CONTENT_URI, null, null, null, null), mResolver); 1432 int count = 0; 1433 try { 1434 while (ei.hasNext()) { 1435 Entity entity = ei.next(); 1436 ContentValues values = entity.getEntityValues(); 1437 assertEquals(CALENDAR_URL, values.getAsString(Calendars.URL)); 1438 ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues(); 1439 switch (values.getAsInteger("_id")) { 1440 case 1: 1441 assertEquals(4, subvalues.size()); // 2 x reminder, 2 x extended properties 1442 break; 1443 case 2: 1444 assertEquals(1, subvalues.size()); // Extended properties 1445 break; 1446 case 3: 1447 assertEquals(1, subvalues.size()); // Attendees 1448 break; 1449 default: 1450 assertEquals(0, subvalues.size()); 1451 break; 1452 } 1453 count += 1; 1454 } 1455 assertEquals(5, count); 1456 } finally { 1457 ei.close(); 1458 } 1459 1460 ei = EventsEntity.newEntityIterator( 1461 mResolver.query(EventsEntity.CONTENT_URI, null, "_id = 3", null, null), 1462 mResolver); 1463 try { 1464 count = 0; 1465 while (ei.hasNext()) { 1466 Entity entity = ei.next(); 1467 count += 1; 1468 } 1469 assertEquals(1, count); 1470 } finally { 1471 ei.close(); 1472 } 1473 } 1474 1475 /** 1476 * Test attendee processing 1477 * @throws Exception 1478 */ 1479 public void testAttendees() throws Exception { 1480 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1481 1482 Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); 1483 long eventId = ContentUris.parseId(eventUri); 1484 1485 ContentValues attendee = new ContentValues(); 1486 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe"); 1487 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com"); 1488 attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED); 1489 attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, 1490 Calendar.Attendees.RELATIONSHIP_ORGANIZER); 1491 attendee.put(Calendar.Attendees.EVENT_ID, eventId); 1492 Uri attendeesUri = mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee); 1493 1494 Cursor cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null, 1495 "event_id=" + eventId, null, null); 1496 assertEquals(1, cursor.getCount()); 1497 cursor.close(); 1498 1499 cursor = mResolver.query(eventUri, null, null, null, null); 1500 int selfColumn = cursor.getColumnIndex(Calendar.Events.SELF_ATTENDEE_STATUS); 1501 cursor.moveToNext(); 1502 long selfAttendeeStatus = cursor.getInt(selfColumn); 1503 assertEquals(Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus); 1504 cursor.close(); 1505 1506 // Change status to declined 1507 attendee.put(Calendar.Attendees.ATTENDEE_STATUS, 1508 Calendar.Attendees.ATTENDEE_STATUS_DECLINED); 1509 mResolver.update(attendeesUri, attendee, null, null); 1510 1511 cursor = mResolver.query(eventUri, null, null, null, null); 1512 cursor.moveToNext(); 1513 selfAttendeeStatus = cursor.getInt(selfColumn); 1514 assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus); 1515 cursor.close(); 1516 1517 // Add another attendee 1518 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Dude"); 1519 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "dude@dude.com"); 1520 attendee.put(Calendar.Attendees.ATTENDEE_STATUS, 1521 Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED); 1522 mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee); 1523 1524 cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null, 1525 "event_id=" + mCalendarId, null, null); 1526 assertEquals(2, cursor.getCount()); 1527 cursor.close(); 1528 1529 cursor = mResolver.query(eventUri, null, null, null, null); 1530 cursor.moveToNext(); 1531 selfAttendeeStatus = cursor.getInt(selfColumn); 1532 assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus); 1533 cursor.close(); 1534 } 1535 1536 /** 1537 * Test the event's _sync_dirty status and clear it. 1538 * @param eventId event to fetch. 1539 * @param wanted the wanted _sync_dirty status 1540 */ 1541 private void testAndClearDirty(long eventId, int wanted) { 1542 Cursor cursor = mResolver.query( 1543 ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId), 1544 null, null, null, null); 1545 try { 1546 assertEquals("Event count", 1, cursor.getCount()); 1547 cursor.moveToNext(); 1548 int dirty = cursor.getInt(cursor.getColumnIndex(Calendar.Events._SYNC_DIRTY)); 1549 assertEquals("dirty flag", wanted, dirty); 1550 if (dirty == 1) { 1551 // Have to access database directly since provider will set dirty again. 1552 mDb.execSQL("UPDATE Events SET _sync_dirty=0 WHERE _id=" + eventId); 1553 } 1554 } finally { 1555 cursor.close(); 1556 } 1557 } 1558 1559 /** 1560 * Test the count of results from a query. 1561 * @param uri The URI to query 1562 * @param where The where string or null. 1563 * @param wanted The number of results wanted. An assertion is thrown if it doesn't match. 1564 */ 1565 private void testQueryCount(Uri uri, String where, int wanted) { 1566 Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */, 1567 null /* sortOrder */); 1568 try { 1569 dumpCursor(cursor); 1570 assertEquals("query results", wanted, cursor.getCount()); 1571 } finally { 1572 cursor.close(); 1573 } 1574 } 1575 1576 /** 1577 * Test dirty flag processing. 1578 * @throws Exception 1579 */ 1580 public void testDirty() throws Exception { 1581 internalTestDirty(false); 1582 } 1583 1584 /** 1585 * Test dirty flag processing for updates from a sync adapter. 1586 * @throws Exception 1587 */ 1588 public void testDirtyWithSyncAdapter() throws Exception { 1589 internalTestDirty(true); 1590 } 1591 1592 /** 1593 * Add CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation. 1594 */ 1595 private Uri updatedUri(Uri uri, boolean syncAdapter) { 1596 if (syncAdapter) { 1597 return uri.buildUpon().appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true") 1598 .build(); 1599 } else { 1600 return uri; 1601 } 1602 } 1603 1604 /** 1605 * Test dirty flag processing either for syncAdapter operations or client operations. 1606 * The main difference is syncAdapter operations don't set the dirty bit. 1607 */ 1608 private void internalTestDirty(boolean syncAdapter) throws Exception { 1609 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1610 1611 Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); 1612 1613 long eventId = ContentUris.parseId(eventUri); 1614 testAndClearDirty(eventId, 1); 1615 1616 ContentValues attendee = new ContentValues(); 1617 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe"); 1618 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com"); 1619 attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED); 1620 attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, 1621 Calendar.Attendees.RELATIONSHIP_ORGANIZER); 1622 attendee.put(Calendar.Attendees.EVENT_ID, eventId); 1623 1624 Uri attendeeUri = mResolver.insert( 1625 updatedUri(Calendar.Attendees.CONTENT_URI, syncAdapter), 1626 attendee); 1627 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1628 testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1); 1629 1630 ContentValues reminder = new ContentValues(); 1631 reminder.put(Calendar.Reminders.MINUTES, 10); 1632 reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_EMAIL); 1633 reminder.put(Calendar.Attendees.EVENT_ID, eventId); 1634 1635 Uri reminderUri = mResolver.insert( 1636 updatedUri(Calendar.Reminders.CONTENT_URI, syncAdapter), reminder); 1637 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1638 testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1); 1639 1640 ContentValues alert = new ContentValues(); 1641 alert.put(Calendar.CalendarAlerts.BEGIN, 10); 1642 alert.put(Calendar.CalendarAlerts.END, 20); 1643 alert.put(Calendar.CalendarAlerts.ALARM_TIME, 30); 1644 alert.put(Calendar.CalendarAlerts.CREATION_TIME, 40); 1645 alert.put(Calendar.CalendarAlerts.RECEIVED_TIME, 50); 1646 alert.put(Calendar.CalendarAlerts.NOTIFY_TIME, 60); 1647 alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.SCHEDULED); 1648 alert.put(Calendar.CalendarAlerts.MINUTES, 30); 1649 alert.put(Calendar.CalendarAlerts.EVENT_ID, eventId); 1650 1651 Uri alertUri = mResolver.insert( 1652 updatedUri(Calendar.CalendarAlerts.CONTENT_URI, syncAdapter), alert); 1653 // Alerts don't dirty the event 1654 testAndClearDirty(eventId, 0); 1655 testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1); 1656 1657 ContentValues extended = new ContentValues(); 1658 extended.put(Calendar.ExtendedProperties.NAME, "foo"); 1659 extended.put(Calendar.ExtendedProperties.VALUE, "bar"); 1660 extended.put(Calendar.ExtendedProperties.EVENT_ID, eventId); 1661 1662 Uri extendedUri = mResolver.insert( 1663 updatedUri(Calendar.ExtendedProperties.CONTENT_URI, syncAdapter), extended); 1664 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1665 testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1); 1666 1667 // Now test updates 1668 1669 attendee = new ContentValues(); 1670 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Sam"); 1671 // Need to include EVENT_ID with attendee update. Is that desired? 1672 attendee.put(Calendar.Attendees.EVENT_ID, eventId); 1673 1674 assertEquals("update", 1, mResolver.update(updatedUri(attendeeUri, syncAdapter), attendee, 1675 null /* where */, null /* selectionArgs */)); 1676 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1677 1678 testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1); 1679 1680 reminder = new ContentValues(); 1681 reminder.put(Calendar.Reminders.MINUTES, 20); 1682 1683 assertEquals("update", 1, mResolver.update(updatedUri(reminderUri, syncAdapter), reminder, 1684 null /* where */, null /* selectionArgs */)); 1685 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1686 testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1); 1687 1688 alert = new ContentValues(); 1689 alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.DISMISSED); 1690 1691 assertEquals("update", 1, mResolver.update(updatedUri(alertUri, syncAdapter), alert, 1692 null /* where */, null /* selectionArgs */)); 1693 // Alerts don't dirty the event 1694 testAndClearDirty(eventId, 0); 1695 testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1); 1696 1697 extended = new ContentValues(); 1698 extended.put(Calendar.ExtendedProperties.VALUE, "baz"); 1699 1700 assertEquals("update", 1, mResolver.update(updatedUri(extendedUri, syncAdapter), extended, 1701 null /* where */, null /* selectionArgs */)); 1702 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1703 testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1); 1704 1705 // Now test deletes 1706 1707 assertEquals("delete", 1, mResolver.delete( 1708 updatedUri(attendeeUri, syncAdapter), 1709 null, null /* selectionArgs */)); 1710 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1711 testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 0); 1712 1713 assertEquals("delete", 1, mResolver.delete(updatedUri(reminderUri, syncAdapter), 1714 null /* where */, null /* selectionArgs */)); 1715 1716 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1717 testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 0); 1718 1719 assertEquals("delete", 1, mResolver.delete(updatedUri(alertUri, syncAdapter), 1720 null /* where */, null /* selectionArgs */)); 1721 1722 // Alerts don't dirty the event 1723 testAndClearDirty(eventId, 0); 1724 testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0); 1725 1726 assertEquals("delete", 1, mResolver.delete(updatedUri(extendedUri, syncAdapter), 1727 null /* where */, null /* selectionArgs */)); 1728 1729 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1730 testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 0); 1731 } 1732 1733 /** 1734 * Test calendar deletion 1735 * @throws Exception 1736 */ 1737 public void testCalendarDeletion() throws Exception { 1738 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1739 Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); 1740 long eventId = ContentUris.parseId(eventUri); 1741 testAndClearDirty(eventId, 1); 1742 Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1")); 1743 long eventId1 = ContentUris.parseId(eventUri); 1744 assertEquals("delete", 1, mResolver.delete(eventUri1, null, null)); 1745 // Calendar has one event and one deleted event 1746 testQueryCount(Calendar.Events.CONTENT_URI, null, 2); 1747 1748 assertEquals("delete", 1, mResolver.delete(Calendar.Calendars.CONTENT_URI, 1749 "_id=" + mCalendarId, null)); 1750 // Calendar should be deleted 1751 testQueryCount(Calendar.Calendars.CONTENT_URI, null, 0); 1752 // Event should be gone 1753 testQueryCount(Calendar.Events.CONTENT_URI, null, 0); 1754 } 1755 1756 /** 1757 * Test multiple account support. 1758 */ 1759 public void testMultipleAccounts() throws Exception { 1760 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1761 int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com"); 1762 Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0")); 1763 Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1"), "user2@google.com"); 1764 1765 testQueryCount(Calendar.Events.CONTENT_URI, null, 2); 1766 Uri eventsWithAccount = Calendar.Events.CONTENT_URI.buildUpon() 1767 .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_NAME, "joe@joe.com") 1768 .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_TYPE, "com.google") 1769 .build(); 1770 // Only one event for that account 1771 testQueryCount(eventsWithAccount, null, 1); 1772 1773 // Test deletion with account and selection 1774 1775 long eventId = ContentUris.parseId(eventUri1); 1776 // Wrong account, should not be deleted 1777 assertEquals("delete", 0, mResolver.delete( 1778 updatedUri(eventsWithAccount, true /* syncAdapter */), 1779 "_id=" + eventId, null /* selectionArgs */)); 1780 testQueryCount(Calendar.Events.CONTENT_URI, null, 2); 1781 // Right account, should be deleted 1782 assertEquals("delete", 1, mResolver.delete( 1783 updatedUri(Calendar.Events.CONTENT_URI, true /* syncAdapter */), 1784 "_id=" + eventId, null /* selectionArgs */)); 1785 testQueryCount(Calendar.Events.CONTENT_URI, null, 1); 1786 } 1787 1788 /** 1789 * Run commands, wiping instance table at each step. 1790 * This tests full instance expansion. 1791 * @throws Exception 1792 */ 1793 public void testCommandSequences1() throws Exception { 1794 commandSequences(true); 1795 } 1796 1797 /** 1798 * Run commands normally. 1799 * This tests incremental instance expansion. 1800 * @throws Exception 1801 */ 1802 public void testCommandSequences2() throws Exception { 1803 commandSequences(false); 1804 } 1805 1806 /** 1807 * Run thorough set of command sequences 1808 * @param wipe true if instances should be wiped and regenerated 1809 * @throws Exception 1810 */ 1811 private void commandSequences(boolean wipe) throws Exception { 1812 Cursor cursor; 1813 Uri url = null; 1814 mWipe = wipe; // Set global flag 1815 1816 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1817 1818 cursor = mResolver.query(mEventsUri, null, null, null, null); 1819 dumpCursor(cursor); 1820 assertEquals(0, cursor.getCount()); 1821 cursor.close(); 1822 Command[] commands; 1823 1824 Log.i(TAG, "Normal insert/delete"); 1825 commands = mNormalInsertDelete; 1826 for (Command command : commands) { 1827 command.execute(); 1828 } 1829 1830 deleteAllEvents(); 1831 1832 Log.i(TAG, "All-day insert/delete"); 1833 commands = mAlldayInsertDelete; 1834 for (Command command : commands) { 1835 command.execute(); 1836 } 1837 1838 deleteAllEvents(); 1839 1840 Log.i(TAG, "Recurring insert/delete"); 1841 commands = mRecurringInsertDelete; 1842 for (Command command : commands) { 1843 command.execute(); 1844 } 1845 1846 deleteAllEvents(); 1847 1848 Log.i(TAG, "Exception with truncated recurrence"); 1849 commands = mExceptionWithTruncatedRecurrence; 1850 for (Command command : commands) { 1851 command.execute(); 1852 } 1853 1854 deleteAllEvents(); 1855 1856 Log.i(TAG, "Exception with moved recurrence"); 1857 commands = mExceptionWithMovedRecurrence; 1858 for (Command command : commands) { 1859 command.execute(); 1860 } 1861 1862 deleteAllEvents(); 1863 1864 Log.i(TAG, "Exception with cancel"); 1865 commands = mCancelInstance; 1866 for (Command command : commands) { 1867 command.execute(); 1868 } 1869 1870 deleteAllEvents(); 1871 1872 Log.i(TAG, "Exception with moved recurrence2"); 1873 commands = mExceptionWithMovedRecurrence2; 1874 for (Command command : commands) { 1875 command.execute(); 1876 } 1877 1878 deleteAllEvents(); 1879 1880 Log.i(TAG, "Exception with no recurrence"); 1881 commands = mExceptionWithNoRecurrence; 1882 for (Command command : commands) { 1883 command.execute(); 1884 } 1885 } 1886 1887 /** 1888 * Test Time toString. 1889 * @throws Exception 1890 */ 1891 // Suppressed because toString currently hangs. 1892 @Suppress 1893 public void testTimeToString() throws Exception { 1894 Time time = new Time(Time.TIMEZONE_UTC); 1895 String str = "2039-01-01T23:00:00.000Z"; 1896 String result = "20390101T230000UTC(0,0,0,-1,0)"; 1897 time.parse3339(str); 1898 assertEquals(result, time.toString()); 1899 } 1900 1901 private Cursor queryInstances(long begin, long end) { 1902 Uri url = Uri.parse("content://calendar/instances/when/" + begin + "/" + end); 1903 return mResolver.query(url, null, null, null, null); 1904 } 1905 1906 private Cursor queryBusyBits(int startDay, int numDays) { 1907 int endDay = startDay + numDays - 1; 1908 Uri url = Uri.parse("content://calendar/busybits/when/" + startDay + "/" + endDay); 1909 return mResolver.query(url, null, null, null, null); 1910 } 1911 1912 protected static class MockProvider extends ContentProvider { 1913 1914 private String mAuthority; 1915 1916 private int mNumItems = 0; 1917 1918 public MockProvider(String authority) { 1919 mAuthority = authority; 1920 } 1921 1922 @Override 1923 public boolean onCreate() { 1924 return true; 1925 } 1926 1927 @Override 1928 public Cursor query(Uri uri, String[] projection, String selection, 1929 String[] selectionArgs, String sortOrder) { 1930 return new ArrayListCursor(new String[]{}, new ArrayList<ArrayList>()); 1931 } 1932 1933 @Override 1934 public String getType(Uri uri) { 1935 throw new UnsupportedOperationException(); 1936 } 1937 1938 @Override 1939 public Uri insert(Uri uri, ContentValues values) { 1940 mNumItems++; 1941 return Uri.parse("content://" + mAuthority + "/" + mNumItems); 1942 } 1943 1944 @Override 1945 public int delete(Uri uri, String selection, String[] selectionArgs) { 1946 return 0; 1947 } 1948 1949 @Override 1950 public int update(Uri uri, ContentValues values, String selection, 1951 String[] selectionArgs) { 1952 return 0; 1953 } 1954 } 1955} 1956