1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.provider.cts;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.Entity;
25 import android.content.EntityIterator;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.database.Cursor;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Environment;
32 import android.provider.CalendarContract;
33 import android.provider.CalendarContract.Attendees;
34 import android.provider.CalendarContract.CalendarEntity;
35 import android.provider.CalendarContract.Calendars;
36 import android.provider.CalendarContract.Colors;
37 import android.provider.CalendarContract.Events;
38 import android.provider.CalendarContract.EventsEntity;
39 import android.provider.CalendarContract.ExtendedProperties;
40 import android.provider.CalendarContract.Instances;
41 import android.provider.CalendarContract.Reminders;
42 import android.provider.CalendarContract.SyncState;
43 import android.test.InstrumentationTestCase;
44 import android.test.suitebuilder.annotation.MediumTest;
45 import android.text.TextUtils;
46 import android.text.format.DateUtils;
47 import android.text.format.Time;
48 import android.util.Log;
49 
50 import com.android.compatibility.common.util.PollingCheck;
51 
52 import java.util.ArrayList;
53 import java.util.HashSet;
54 import java.util.List;
55 import java.util.Set;
56 
57 public class CalendarTest extends InstrumentationTestCase {
58 
59     private static final String TAG = "CalCTS";
60     private static final String CTS_TEST_TYPE = "LOCAL";
61 
62     // an arbitrary int used by some tests
63     private static final int SOME_ARBITRARY_INT = 143234;
64 
65     // 15 sec timeout for reminder broadcast (but shouldn't usually take this long).
66     private static final int POLLING_TIMEOUT = 15000;
67 
68     // @formatter:off
69     private static final String[] TIME_ZONES = new String[] {
70             "UTC",
71             "America/Los_Angeles",
72             "Asia/Beirut",
73             "Pacific/Auckland", };
74     // @formatter:on
75 
76     private static final String SQL_WHERE_ID = Events._ID + "=?";
77     private static final String SQL_WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?";
78 
79     private ContentResolver mContentResolver;
80 
81     /** If set, log verbose instance info when running recurrence tests. */
82     private static final boolean DEBUG_RECURRENCE = false;
83 
84     private static class CalendarHelper {
85 
86         // @formatter:off
87         public static final String[] CALENDARS_SYNC_PROJECTION = new String[] {
88                 Calendars._ID,
89                 Calendars.ACCOUNT_NAME,
90                 Calendars.ACCOUNT_TYPE,
91                 Calendars._SYNC_ID,
92                 Calendars.CAL_SYNC7,
93                 Calendars.CAL_SYNC8,
94                 Calendars.DIRTY,
95                 Calendars.NAME,
96                 Calendars.CALENDAR_DISPLAY_NAME,
97                 Calendars.CALENDAR_COLOR,
98                 Calendars.CALENDAR_COLOR_KEY,
99                 Calendars.CALENDAR_ACCESS_LEVEL,
100                 Calendars.VISIBLE,
101                 Calendars.SYNC_EVENTS,
102                 Calendars.CALENDAR_LOCATION,
103                 Calendars.CALENDAR_TIME_ZONE,
104                 Calendars.OWNER_ACCOUNT,
105                 Calendars.CAN_ORGANIZER_RESPOND,
106                 Calendars.CAN_MODIFY_TIME_ZONE,
107                 Calendars.MAX_REMINDERS,
108                 Calendars.ALLOWED_REMINDERS,
109                 Calendars.ALLOWED_AVAILABILITY,
110                 Calendars.ALLOWED_ATTENDEE_TYPES,
111                 Calendars.DELETED,
112                 Calendars.CAL_SYNC1,
113                 Calendars.CAL_SYNC2,
114                 Calendars.CAL_SYNC3,
115                 Calendars.CAL_SYNC4,
116                 Calendars.CAL_SYNC5,
117                 Calendars.CAL_SYNC6,
118                 };
119         // @formatter:on
120 
CalendarHelper()121         private CalendarHelper() {}     // do not instantiate this class
122 
123         /**
124          * Generates the e-mail address for the Calendar owner.  Use this for
125          * Calendars.OWNER_ACCOUNT, Events.OWNER_ACCOUNT, and for Attendees.ATTENDEE_EMAIL
126          * when you want a "self" attendee entry.
127          */
generateCalendarOwnerEmail(String account)128         static String generateCalendarOwnerEmail(String account) {
129             return "OWNER_" + account + "@example.com";
130         }
131 
132         /**
133          * Creates a new set of values for creating a single calendar with every
134          * field.
135          *
136          * @param account The account name to create this calendar with
137          * @param seed A number used to generate the values
138          * @return A complete set of values for the calendar
139          */
getNewCalendarValues( String account, int seed)140         public static ContentValues getNewCalendarValues(
141                 String account, int seed) {
142             String seedString = Long.toString(seed);
143             ContentValues values = new ContentValues();
144             values.put(Calendars.ACCOUNT_TYPE, CTS_TEST_TYPE);
145 
146             values.put(Calendars.ACCOUNT_NAME, account);
147             values.put(Calendars._SYNC_ID, "SYNC_ID:" + seedString);
148             values.put(Calendars.CAL_SYNC7, "SYNC_V:" + seedString);
149             values.put(Calendars.CAL_SYNC8, "SYNC_TIME:" + seedString);
150             values.put(Calendars.DIRTY, 0);
151             values.put(Calendars.OWNER_ACCOUNT, generateCalendarOwnerEmail(account));
152 
153             values.put(Calendars.NAME, seedString);
154             values.put(Calendars.CALENDAR_DISPLAY_NAME, "DISPLAY_" + seedString);
155 
156             values.put(Calendars.CALENDAR_ACCESS_LEVEL, (seed % 8) * 100);
157 
158             values.put(Calendars.CALENDAR_COLOR, 0xff000000 + seed);
159             values.put(Calendars.VISIBLE, seed % 2);
160             values.put(Calendars.SYNC_EVENTS, 1);   // must be 1 for recurrence expansion
161             values.put(Calendars.CALENDAR_LOCATION, "LOCATION:" + seedString);
162             values.put(Calendars.CALENDAR_TIME_ZONE, TIME_ZONES[seed % TIME_ZONES.length]);
163             values.put(Calendars.CAN_ORGANIZER_RESPOND, seed % 2);
164             values.put(Calendars.CAN_MODIFY_TIME_ZONE, seed % 2);
165             values.put(Calendars.MAX_REMINDERS, 3);
166             values.put(Calendars.ALLOWED_REMINDERS, "0,1,2");   // does not include SMS (3)
167             values.put(Calendars.ALLOWED_ATTENDEE_TYPES, "0,1,2,3");
168             values.put(Calendars.ALLOWED_AVAILABILITY, "0,1,2,3");
169             values.put(Calendars.CAL_SYNC1, "SYNC1:" + seedString);
170             values.put(Calendars.CAL_SYNC2, "SYNC2:" + seedString);
171             values.put(Calendars.CAL_SYNC3, "SYNC3:" + seedString);
172             values.put(Calendars.CAL_SYNC4, "SYNC4:" + seedString);
173             values.put(Calendars.CAL_SYNC5, "SYNC5:" + seedString);
174             values.put(Calendars.CAL_SYNC6, "SYNC6:" + seedString);
175 
176             return values;
177         }
178 
179         /**
180          * Creates a set of values with just the updates and modifies the
181          * original values to the expected values
182          */
getUpdateCalendarValuesWithOriginal( ContentValues original, int seed)183         public static ContentValues getUpdateCalendarValuesWithOriginal(
184                 ContentValues original, int seed) {
185             ContentValues values = new ContentValues();
186             String seedString = Long.toString(seed);
187 
188             values.put(Calendars.CALENDAR_DISPLAY_NAME, "DISPLAY_" + seedString);
189             values.put(Calendars.CALENDAR_COLOR, 0xff000000 + seed);
190             values.put(Calendars.VISIBLE, seed % 2);
191             values.put(Calendars.SYNC_EVENTS, seed % 2);
192 
193             original.putAll(values);
194             original.put(Calendars.DIRTY, 1);
195 
196             return values;
197         }
198 
deleteCalendarById(ContentResolver resolver, long id)199         public static int deleteCalendarById(ContentResolver resolver, long id) {
200             return resolver.delete(Calendars.CONTENT_URI, Calendars._ID + "=?",
201                     new String[] { Long.toString(id) });
202         }
203 
deleteCalendarByAccount(ContentResolver resolver, String account)204         public static int deleteCalendarByAccount(ContentResolver resolver, String account) {
205             return resolver.delete(Calendars.CONTENT_URI, Calendars.ACCOUNT_NAME + "=?",
206                     new String[] { account });
207         }
208 
getCalendarsByAccount(ContentResolver resolver, String account)209         public static Cursor getCalendarsByAccount(ContentResolver resolver, String account) {
210             String selection = Calendars.ACCOUNT_TYPE + "=?";
211             String[] selectionArgs;
212             if (account != null) {
213                 selection += " AND " + Calendars.ACCOUNT_NAME + "=?";
214                 selectionArgs = new String[2];
215                 selectionArgs[1] = account;
216             } else {
217                 selectionArgs = new String[1];
218             }
219             selectionArgs[0] = CTS_TEST_TYPE;
220 
221             return resolver.query(Calendars.CONTENT_URI, CALENDARS_SYNC_PROJECTION, selection,
222                     selectionArgs, null);
223         }
224     }
225 
226     /**
227      * Helper class for manipulating entries in the _sync_state table.
228      */
229     private static class SyncStateHelper {
230         public static final String[] SYNCSTATE_PROJECTION = new String[] {
231             SyncState._ID,
232             SyncState.ACCOUNT_NAME,
233             SyncState.ACCOUNT_TYPE,
234             SyncState.DATA
235         };
236 
237         private static final byte[] SAMPLE_SYNC_DATA = {
238             (byte) 'H', (byte) 'e', (byte) 'l', (byte) 'l', (byte) 'o'
239         };
240 
SyncStateHelper()241         private SyncStateHelper() {}      // do not instantiate
242 
243         /**
244          * Creates a new set of values for creating a new _sync_state entry.
245          */
getNewSyncStateValues(String account)246         public static ContentValues getNewSyncStateValues(String account) {
247             ContentValues values = new ContentValues();
248             values.put(SyncState.DATA, SAMPLE_SYNC_DATA);
249             values.put(SyncState.ACCOUNT_NAME, account);
250             values.put(SyncState.ACCOUNT_TYPE, CTS_TEST_TYPE);
251             return values;
252         }
253 
254         /**
255          * Retrieves the _sync_state entry with the specified ID.
256          */
getSyncStateById(ContentResolver resolver, long id)257         public static Cursor getSyncStateById(ContentResolver resolver, long id) {
258             Uri uri = ContentUris.withAppendedId(SyncState.CONTENT_URI, id);
259             return resolver.query(uri, SYNCSTATE_PROJECTION, null, null, null);
260         }
261 
262         /**
263          * Retrieves the _sync_state entry for the specified account.
264          */
getSyncStateByAccount(ContentResolver resolver, String account)265         public static Cursor getSyncStateByAccount(ContentResolver resolver, String account) {
266             assertNotNull(account);
267             String selection = SyncState.ACCOUNT_TYPE + "=? AND " + SyncState.ACCOUNT_NAME + "=?";
268             String[] selectionArgs = new String[] { CTS_TEST_TYPE, account };
269 
270             return resolver.query(SyncState.CONTENT_URI, SYNCSTATE_PROJECTION, selection,
271                     selectionArgs, null);
272         }
273 
274         /**
275          * Deletes the _sync_state entry with the specified ID.  Always done as app.
276          */
deleteSyncStateById(ContentResolver resolver, long id)277         public static int deleteSyncStateById(ContentResolver resolver, long id) {
278             Uri uri = ContentUris.withAppendedId(SyncState.CONTENT_URI, id);
279             return resolver.delete(uri, null, null);
280         }
281 
282         /**
283          * Deletes the _sync_state entry associated with the specified account.  Can be done
284          * as app or sync adapter.
285          */
deleteSyncStateByAccount(ContentResolver resolver, String account, boolean asSyncAdapter)286         public static int deleteSyncStateByAccount(ContentResolver resolver, String account,
287                 boolean asSyncAdapter) {
288             Uri uri = SyncState.CONTENT_URI;
289             if (asSyncAdapter) {
290                 uri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
291             }
292             return resolver.delete(uri, SyncState.ACCOUNT_NAME + "=?",
293                     new String[] { account });
294         }
295     }
296 
297     // @formatter:off
298     private static class EventHelper {
299         public static final String[] EVENTS_PROJECTION = new String[] {
300             Events._ID,
301             Events.ACCOUNT_NAME,
302             Events.ACCOUNT_TYPE,
303             Events.OWNER_ACCOUNT,
304             // Events.ORGANIZER_CAN_RESPOND, from Calendars
305             // Events.CAN_CHANGE_TZ, from Calendars
306             // Events.MAX_REMINDERS, from Calendars
307             Events.CALENDAR_ID,
308             // Events.CALENDAR_DISPLAY_NAME, from Calendars
309             // Events.CALENDAR_COLOR, from Calendars
310             // Events.CALENDAR_ACL, from Calendars
311             // Events.CALENDAR_VISIBLE, from Calendars
312             Events.SYNC_DATA3,
313             Events.SYNC_DATA6,
314             Events.TITLE,
315             Events.EVENT_LOCATION,
316             Events.DESCRIPTION,
317             Events.STATUS,
318             Events.SELF_ATTENDEE_STATUS,
319             Events.DTSTART,
320             Events.DTEND,
321             Events.EVENT_TIMEZONE,
322             Events.EVENT_END_TIMEZONE,
323             Events.EVENT_COLOR,
324             Events.EVENT_COLOR_KEY,
325             Events.DURATION,
326             Events.ALL_DAY,
327             Events.ACCESS_LEVEL,
328             Events.AVAILABILITY,
329             Events.HAS_ALARM,
330             Events.HAS_EXTENDED_PROPERTIES,
331             Events.RRULE,
332             Events.RDATE,
333             Events.EXRULE,
334             Events.EXDATE,
335             Events.ORIGINAL_ID,
336             Events.ORIGINAL_SYNC_ID,
337             Events.ORIGINAL_INSTANCE_TIME,
338             Events.ORIGINAL_ALL_DAY,
339             Events.LAST_DATE,
340             Events.HAS_ATTENDEE_DATA,
341             Events.GUESTS_CAN_MODIFY,
342             Events.GUESTS_CAN_INVITE_OTHERS,
343             Events.GUESTS_CAN_SEE_GUESTS,
344             Events.ORGANIZER,
345             Events.DELETED,
346             Events._SYNC_ID,
347             Events.SYNC_DATA4,
348             Events.SYNC_DATA5,
349             Events.DIRTY,
350             Events.SYNC_DATA8,
351             Events.SYNC_DATA2,
352             Events.SYNC_DATA1,
353             Events.SYNC_DATA2,
354             Events.SYNC_DATA3,
355             Events.SYNC_DATA4,
356             Events.MUTATORS,
357         };
358         // @formatter:on
359 
EventHelper()360         private EventHelper() {}    // do not instantiate this class
361 
362         /**
363          * Constructs a set of name/value pairs that can be used to create a Calendar event.
364          * Various fields are generated from the seed value.
365          */
getNewEventValues( String account, int seed, long calendarId, boolean asSyncAdapter)366         public static ContentValues getNewEventValues(
367                 String account, int seed, long calendarId, boolean asSyncAdapter) {
368             String seedString = Long.toString(seed);
369             ContentValues values = new ContentValues();
370             values.put(Events.ORGANIZER, "ORGANIZER:" + seedString);
371 
372             values.put(Events.TITLE, "TITLE:" + seedString);
373             values.put(Events.EVENT_LOCATION, "LOCATION_" + seedString);
374 
375             values.put(Events.CALENDAR_ID, calendarId);
376 
377             values.put(Events.DESCRIPTION, "DESCRIPTION:" + seedString);
378             values.put(Events.STATUS, seed % 2);    // avoid STATUS_CANCELED for general testing
379 
380             values.put(Events.DTSTART, seed);
381             values.put(Events.DTEND, seed + DateUtils.HOUR_IN_MILLIS);
382             values.put(Events.EVENT_TIMEZONE, TIME_ZONES[seed % TIME_ZONES.length]);
383             values.put(Events.EVENT_COLOR, seed);
384             // values.put(Events.EVENT_TIMEZONE2, TIME_ZONES[(seed +1) %
385             // TIME_ZONES.length]);
386             if ((seed % 2) == 0) {
387                 // Either set to zero, or leave unset to get default zero.
388                 // Must be 0 or dtstart/dtend will get adjusted.
389                 values.put(Events.ALL_DAY, 0);
390             }
391             values.put(Events.ACCESS_LEVEL, seed % 4);
392             values.put(Events.AVAILABILITY, seed % 2);
393             values.put(Events.HAS_EXTENDED_PROPERTIES, seed % 2);
394             values.put(Events.HAS_ATTENDEE_DATA, seed % 2);
395             values.put(Events.GUESTS_CAN_MODIFY, seed % 2);
396             values.put(Events.GUESTS_CAN_INVITE_OTHERS, seed % 2);
397             values.put(Events.GUESTS_CAN_SEE_GUESTS, seed % 2);
398 
399             // Default is STATUS_TENTATIVE (0).  We either set it to that explicitly, or leave
400             // it set to the default.
401             if (seed != Events.STATUS_TENTATIVE) {
402                 values.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE);
403             }
404 
405             if (asSyncAdapter) {
406                 values.put(Events._SYNC_ID, "SYNC_ID:" + seedString);
407                 values.put(Events.SYNC_DATA4, "SYNC_V:" + seedString);
408                 values.put(Events.SYNC_DATA5, "SYNC_TIME:" + seedString);
409                 values.put(Events.SYNC_DATA3, "HTML:" + seedString);
410                 values.put(Events.SYNC_DATA6, "COMMENTS:" + seedString);
411                 values.put(Events.DIRTY, 0);
412                 values.put(Events.SYNC_DATA8, "0");
413             } else {
414                 // only the sync adapter can set the DIRTY flag
415                 //values.put(Events.DIRTY, 1);
416             }
417             // values.put(Events.SYNC1, "SYNC1:" + seedString);
418             // values.put(Events.SYNC2, "SYNC2:" + seedString);
419             // values.put(Events.SYNC3, "SYNC3:" + seedString);
420             // values.put(Events.SYNC4, "SYNC4:" + seedString);
421             // values.put(Events.SYNC5, "SYNC5:" + seedString);
422 //            Events.RRULE,
423 //            Events.RDATE,
424 //            Events.EXRULE,
425 //            Events.EXDATE,
426 //            // Events.ORIGINAL_ID
427 //            Events.ORIGINAL_EVENT, // rename ORIGINAL_SYNC_ID
428 //            Events.ORIGINAL_INSTANCE_TIME,
429 //            Events.ORIGINAL_ALL_DAY,
430 
431             return values;
432         }
433 
434         /**
435          * Constructs a set of name/value pairs that can be used to create a recurring
436          * Calendar event.
437          *
438          * A duration of "P1D" is treated as an all-day event.
439          *
440          * @param startWhen Starting date/time in RFC 3339 format
441          * @param duration Event duration, in RFC 2445 duration format
442          * @param rrule Recurrence rule
443          * @return name/value pairs to use when creating event
444          */
getNewRecurringEventValues(String account, int seed, long calendarId, boolean asSyncAdapter, String startWhen, String duration, String rrule)445         public static ContentValues getNewRecurringEventValues(String account, int seed,
446                 long calendarId, boolean asSyncAdapter, String startWhen, String duration,
447                 String rrule) {
448 
449             // Set up some general stuff.
450             ContentValues values = getNewEventValues(account, seed, calendarId, asSyncAdapter);
451 
452             // Replace the DTSTART field.
453             String timeZone = values.getAsString(Events.EVENT_TIMEZONE);
454             Time time = new Time(timeZone);
455             time.parse3339(startWhen);
456             values.put(Events.DTSTART, time.toMillis(false));
457 
458             // Add in the recurrence-specific fields, and drop DTEND.
459             values.put(Events.RRULE, rrule);
460             values.put(Events.DURATION, duration);
461             values.remove(Events.DTEND);
462 
463             return values;
464         }
465 
466         /**
467          * Constructs the basic name/value pairs required for an exception to a recurring event.
468          *
469          * @param instanceStartMillis The start time of the instance
470          * @return name/value pairs to use when creating event
471          */
getNewExceptionValues(long instanceStartMillis)472         public static ContentValues getNewExceptionValues(long instanceStartMillis) {
473             ContentValues values = new ContentValues();
474             values.put(Events.ORIGINAL_INSTANCE_TIME, instanceStartMillis);
475 
476             return values;
477         }
478 
getUpdateEventValuesWithOriginal(ContentValues original, int seed, boolean asSyncAdapter)479         public static ContentValues getUpdateEventValuesWithOriginal(ContentValues original,
480                 int seed, boolean asSyncAdapter) {
481             String seedString = Long.toString(seed);
482             ContentValues values = new ContentValues();
483 
484             values.put(Events.TITLE, "TITLE:" + seedString);
485             values.put(Events.EVENT_LOCATION, "LOCATION_" + seedString);
486             values.put(Events.DESCRIPTION, "DESCRIPTION:" + seedString);
487             values.put(Events.STATUS, seed % 3);
488 
489             values.put(Events.DTSTART, seed);
490             values.put(Events.DTEND, seed + DateUtils.HOUR_IN_MILLIS);
491             values.put(Events.EVENT_TIMEZONE, TIME_ZONES[seed % TIME_ZONES.length]);
492             // values.put(Events.EVENT_TIMEZONE2, TIME_ZONES[(seed +1) %
493             // TIME_ZONES.length]);
494             values.put(Events.ACCESS_LEVEL, seed % 4);
495             values.put(Events.AVAILABILITY, seed % 2);
496             values.put(Events.HAS_EXTENDED_PROPERTIES, seed % 2);
497             values.put(Events.HAS_ATTENDEE_DATA, seed % 2);
498             values.put(Events.GUESTS_CAN_MODIFY, seed % 2);
499             values.put(Events.GUESTS_CAN_INVITE_OTHERS, seed % 2);
500             values.put(Events.GUESTS_CAN_SEE_GUESTS, seed % 2);
501             if (asSyncAdapter) {
502                 values.put(Events._SYNC_ID, "SYNC_ID:" + seedString);
503                 values.put(Events.SYNC_DATA4, "SYNC_V:" + seedString);
504                 values.put(Events.SYNC_DATA5, "SYNC_TIME:" + seedString);
505                 values.put(Events.DIRTY, 0);
506             }
507             original.putAll(values);
508             return values;
509         }
510 
addDefaultReadOnlyValues(ContentValues values, String account, boolean asSyncAdapter)511         public static void addDefaultReadOnlyValues(ContentValues values, String account,
512                 boolean asSyncAdapter) {
513             values.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE);
514             values.put(Events.DELETED, 0);
515             values.put(Events.DIRTY, asSyncAdapter ? 0 : 1);
516             values.put(Events.OWNER_ACCOUNT, CalendarHelper.generateCalendarOwnerEmail(account));
517             values.put(Events.ACCOUNT_TYPE, CTS_TEST_TYPE);
518             values.put(Events.ACCOUNT_NAME, account);
519         }
520 
521         /**
522          * Generates a RFC2445-format duration string.
523          */
generateDurationString(long durationMillis, boolean isAllDay)524         private static String generateDurationString(long durationMillis, boolean isAllDay) {
525             long durationSeconds = durationMillis / 1000;
526 
527             // The server may react differently to an all-day event specified as "P1D" than
528             // it will to "PT86400S"; see b/1594638.
529             if (isAllDay && (durationSeconds % 86400) == 0) {
530                 return "P" + durationSeconds / 86400 + "D";
531             } else {
532                 return "PT" + durationSeconds + "S";
533             }
534         }
535 
536         /**
537          * Deletes the event, and updates the values.
538          * @param resolver The resolver to issue the query against.
539          * @param uri The deletion URI.
540          * @param values Set of values to update (sets DELETED and DIRTY).
541          * @return The number of rows modified.
542          */
deleteEvent(ContentResolver resolver, Uri uri, ContentValues values)543         public static int deleteEvent(ContentResolver resolver, Uri uri, ContentValues values) {
544             values.put(Events.DELETED, 1);
545             values.put(Events.DIRTY, 1);
546             return resolver.delete(uri, null, null);
547         }
548 
deleteEventAsSyncAdapter(ContentResolver resolver, Uri uri, String account)549         public static int deleteEventAsSyncAdapter(ContentResolver resolver, Uri uri,
550                 String account) {
551             Uri syncUri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
552             return resolver.delete(syncUri, null, null);
553         }
554 
getEventsByAccount(ContentResolver resolver, String account)555         public static Cursor getEventsByAccount(ContentResolver resolver, String account) {
556             String selection = Calendars.ACCOUNT_TYPE + "=?";
557             String[] selectionArgs;
558             if (account != null) {
559                 selection += " AND " + Calendars.ACCOUNT_NAME + "=?";
560                 selectionArgs = new String[2];
561                 selectionArgs[1] = account;
562             } else {
563                 selectionArgs = new String[1];
564             }
565             selectionArgs[0] = CTS_TEST_TYPE;
566             return resolver.query(Events.CONTENT_URI, EVENTS_PROJECTION, selection, selectionArgs,
567                     null);
568         }
569 
getEventByUri(ContentResolver resolver, Uri uri)570         public static Cursor getEventByUri(ContentResolver resolver, Uri uri) {
571             return resolver.query(uri, EVENTS_PROJECTION, null, null, null);
572         }
573 
574         /**
575          * Looks up the specified Event in the database and returns the "selfAttendeeStatus"
576          * value.
577          */
lookupSelfAttendeeStatus(ContentResolver resolver, long eventId)578         public static int lookupSelfAttendeeStatus(ContentResolver resolver, long eventId) {
579             return getIntFromDatabase(resolver, Events.CONTENT_URI, eventId,
580                     Events.SELF_ATTENDEE_STATUS);
581         }
582 
583         /**
584          * Looks up the specified Event in the database and returns the "hasAlarm"
585          * value.
586          */
lookupHasAlarm(ContentResolver resolver, long eventId)587         public static int lookupHasAlarm(ContentResolver resolver, long eventId) {
588             return getIntFromDatabase(resolver, Events.CONTENT_URI, eventId,
589                     Events.HAS_ALARM);
590         }
591     }
592 
593     /**
594      * Helper class for manipulating entries in the Attendees table.
595      */
596     private static class AttendeeHelper {
597         public static final String[] ATTENDEES_PROJECTION = new String[] {
598             Attendees._ID,
599             Attendees.EVENT_ID,
600             Attendees.ATTENDEE_NAME,
601             Attendees.ATTENDEE_EMAIL,
602             Attendees.ATTENDEE_STATUS,
603             Attendees.ATTENDEE_RELATIONSHIP,
604             Attendees.ATTENDEE_TYPE
605         };
606         // indexes into projection
607         public static final int ATTENDEES_ID_INDEX = 0;
608         public static final int ATTENDEES_EVENT_ID_INDEX = 1;
609 
610         // do not instantiate
AttendeeHelper()611         private AttendeeHelper() {}
612 
613         /**
614          * Adds a new attendee to the specified event.
615          *
616          * @return the _id of the new attendee, or -1 on failure
617          */
addAttendee(ContentResolver resolver, long eventId, String name, String email, int status, int relationship, int type)618         public static long addAttendee(ContentResolver resolver, long eventId, String name,
619                 String email, int status, int relationship, int type) {
620             Uri uri = Attendees.CONTENT_URI;
621 
622             ContentValues attendee = new ContentValues();
623             attendee.put(Attendees.EVENT_ID, eventId);
624             attendee.put(Attendees.ATTENDEE_NAME, name);
625             attendee.put(Attendees.ATTENDEE_EMAIL, email);
626             attendee.put(Attendees.ATTENDEE_STATUS, status);
627             attendee.put(Attendees.ATTENDEE_RELATIONSHIP, relationship);
628             attendee.put(Attendees.ATTENDEE_TYPE, type);
629             Uri result = resolver.insert(uri, attendee);
630             return ContentUris.parseId(result);
631         }
632 
633         /**
634          * Finds all Attendees rows for the specified event and email address.  The returned
635          * cursor will use {@link AttendeeHelper#ATTENDEES_PROJECTION}.
636          */
findAttendeesByEmail(ContentResolver resolver, long eventId, String email)637         public static Cursor findAttendeesByEmail(ContentResolver resolver, long eventId,
638                 String email) {
639             return resolver.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION,
640                     Attendees.EVENT_ID + "=? AND " + Attendees.ATTENDEE_EMAIL + "=?",
641                     new String[] { String.valueOf(eventId), email }, null);
642         }
643     }
644 
645     /**
646      * Helper class for manipulating entries in the Colors table.
647      */
648     private static class ColorHelper {
649         public static final String WHERE_COLOR_ACCOUNT = Colors.ACCOUNT_NAME + "=? AND "
650                 + Colors.ACCOUNT_TYPE + "=?";
651         public static final String WHERE_COLOR_ACCOUNT_AND_INDEX = WHERE_COLOR_ACCOUNT + " AND "
652                 + Colors.COLOR_KEY + "=?";
653 
654         public static final String[] COLORS_PROJECTION = new String[] {
655                 Colors._ID, // 0
656                 Colors.ACCOUNT_NAME, // 1
657                 Colors.ACCOUNT_TYPE, // 2
658                 Colors.DATA, // 3
659                 Colors.COLOR_TYPE, // 4
660                 Colors.COLOR_KEY, // 5
661                 Colors.COLOR, // 6
662         };
663         // indexes into projection
664         public static final int COLORS_ID_INDEX = 0;
665         public static final int COLORS_INDEX_INDEX = 5;
666         public static final int COLORS_COLOR_INDEX = 6;
667 
668         public static final int[] DEFAULT_TYPES = new int[] {
669                 Colors.TYPE_CALENDAR, Colors.TYPE_CALENDAR, Colors.TYPE_CALENDAR,
670                 Colors.TYPE_CALENDAR, Colors.TYPE_EVENT, Colors.TYPE_EVENT, Colors.TYPE_EVENT,
671                 Colors.TYPE_EVENT,
672         };
673         public static final int[] DEFAULT_COLORS = new int[] {
674                 0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFFAA00AA, 0xFF00AAAA, 0xFF333333, 0xFFAAAA00,
675                 0xFFAAAAAA,
676         };
677         public static final String[] DEFAULT_INDICES = new String[] {
678                 "000", "001", "010", "011", "100", "101", "110", "111",
679         };
680 
681         public static final int C_COLOR_0 = 0;
682         public static final int C_COLOR_1 = 1;
683         public static final int C_COLOR_2 = 2;
684         public static final int C_COLOR_3 = 3;
685         public static final int E_COLOR_0 = 4;
686         public static final int E_COLOR_1 = 5;
687         public static final int E_COLOR_2 = 6;
688         public static final int E_COLOR_3 = 7;
689 
690         // do not instantiate
ColorHelper()691         private ColorHelper() {
692         }
693 
694         /**
695          * Adds a new color to the colors table.
696          *
697          * @return the _id of the new color, or -1 on failure
698          */
addColor(ContentResolver resolver, String accountName, String accountType, String data, String index, int type, int color)699         public static long addColor(ContentResolver resolver, String accountName,
700                 String accountType, String data, String index, int type, int color) {
701             Uri uri = asSyncAdapter(Colors.CONTENT_URI, accountName, accountType);
702 
703             ContentValues colorValues = new ContentValues();
704             colorValues.put(Colors.DATA, data);
705             colorValues.put(Colors.COLOR_KEY, index);
706             colorValues.put(Colors.COLOR_TYPE, type);
707             colorValues.put(Colors.COLOR, color);
708             Uri result = resolver.insert(uri, colorValues);
709             return ContentUris.parseId(result);
710         }
711 
712         /**
713          * Finds the color specified by an account name/type and a color index.
714          * The returned cursor will use {@link ColorHelper#COLORS_PROJECTION}.
715          */
findColorByIndex(ContentResolver resolver, String accountName, String accountType, String index)716         public static Cursor findColorByIndex(ContentResolver resolver, String accountName,
717                 String accountType, String index) {
718             return resolver.query(Colors.CONTENT_URI, COLORS_PROJECTION,
719                     WHERE_COLOR_ACCOUNT_AND_INDEX,
720                     new String[] {accountName, accountType, index}, null);
721         }
722 
findColorsByAccount(ContentResolver resolver, String accountName, String accountType)723         public static Cursor findColorsByAccount(ContentResolver resolver, String accountName,
724                 String accountType) {
725             return resolver.query(Colors.CONTENT_URI, COLORS_PROJECTION, WHERE_COLOR_ACCOUNT,
726                     new String[] { accountName, accountType }, null);
727         }
728 
729         /**
730          * Adds a default set of test colors to the Colors table under the given
731          * account.
732          *
733          * @return true if the default colors were added successfully
734          */
addDefaultColorsToAccount(ContentResolver resolver, String accountName, String accountType)735         public static boolean addDefaultColorsToAccount(ContentResolver resolver,
736                 String accountName, String accountType) {
737             for (int i = 0; i < DEFAULT_INDICES.length; i++) {
738                 long id = addColor(resolver, accountName, accountType, null, DEFAULT_INDICES[i],
739                         DEFAULT_TYPES[i], DEFAULT_COLORS[i]);
740                 if (id == -1) {
741                     return false;
742                 }
743             }
744             return true;
745         }
746 
deleteColorsByAccount(ContentResolver resolver, String accountName, String accountType)747         public static void deleteColorsByAccount(ContentResolver resolver, String accountName,
748                 String accountType) {
749             Uri uri = asSyncAdapter(Colors.CONTENT_URI, accountName, accountType);
750             resolver.delete(uri, WHERE_COLOR_ACCOUNT, new String[] { accountName, accountType });
751         }
752     }
753 
754 
755     /**
756      * Helper class for manipulating entries in the Reminders table.
757      */
758     private static class ReminderHelper {
759         public static final String[] REMINDERS_PROJECTION = new String[] {
760             Reminders._ID,
761             Reminders.EVENT_ID,
762             Reminders.MINUTES,
763             Reminders.METHOD
764         };
765         // indexes into projection
766         public static final int REMINDERS_ID_INDEX = 0;
767         public static final int REMINDERS_EVENT_ID_INDEX = 1;
768         public static final int REMINDERS_MINUTES_INDEX = 2;
769         public static final int REMINDERS_METHOD_INDEX = 3;
770 
771         // do not instantiate
ReminderHelper()772         private ReminderHelper() {}
773 
774         /**
775          * Adds a new reminder to the specified event.
776          *
777          * @return the _id of the new reminder, or -1 on failure
778          */
addReminder(ContentResolver resolver, long eventId, int minutes, int method)779         public static long addReminder(ContentResolver resolver, long eventId, int minutes,
780                 int method) {
781             Uri uri = Reminders.CONTENT_URI;
782 
783             ContentValues reminder = new ContentValues();
784             reminder.put(Reminders.EVENT_ID, eventId);
785             reminder.put(Reminders.MINUTES, minutes);
786             reminder.put(Reminders.METHOD, method);
787             Uri result = resolver.insert(uri, reminder);
788             return ContentUris.parseId(result);
789         }
790 
791         /**
792          * Finds all Reminders rows for the specified event.  The returned cursor will use
793          * {@link ReminderHelper#REMINDERS_PROJECTION}.
794          */
findRemindersByEventId(ContentResolver resolver, long eventId)795         public static Cursor findRemindersByEventId(ContentResolver resolver, long eventId) {
796             return resolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION,
797                     Reminders.EVENT_ID + "=?", new String[] { String.valueOf(eventId) }, null);
798         }
799 
800         /**
801          * Looks up the specified Reminders row and returns the "method" value.
802          */
lookupMethod(ContentResolver resolver, long remId)803         public static int lookupMethod(ContentResolver resolver, long remId) {
804             return getIntFromDatabase(resolver, Reminders.CONTENT_URI, remId,
805                     Reminders.METHOD);
806         }
807    }
808 
809     /**
810      * Helper class for manipulating entries in the ExtendedProperties table.
811      */
812     private static class ExtendedPropertiesHelper {
813         public static final String[] EXTENDED_PROPERTIES_PROJECTION = new String[] {
814             ExtendedProperties._ID,
815             ExtendedProperties.EVENT_ID,
816             ExtendedProperties.NAME,
817             ExtendedProperties.VALUE
818         };
819         // indexes into projection
820         public static final int EXTENDED_PROPERTIES_ID_INDEX = 0;
821         public static final int EXTENDED_PROPERTIES_EVENT_ID_INDEX = 1;
822         public static final int EXTENDED_PROPERTIES_NAME_INDEX = 2;
823         public static final int EXTENDED_PROPERTIES_VALUE_INDEX = 3;
824 
825         // do not instantiate
ExtendedPropertiesHelper()826         private ExtendedPropertiesHelper() {}
827 
828         /**
829          * Adds a new ExtendedProperty for the specified event.  Runs as sync adapter.
830          *
831          * @return the _id of the new ExtendedProperty, or -1 on failure
832          */
addExtendedProperty(ContentResolver resolver, String account, long eventId, String name, String value)833         public static long addExtendedProperty(ContentResolver resolver, String account,
834                 long eventId, String name, String value) {
835              Uri uri = asSyncAdapter(ExtendedProperties.CONTENT_URI, account, CTS_TEST_TYPE);
836 
837             ContentValues ep = new ContentValues();
838             ep.put(ExtendedProperties.EVENT_ID, eventId);
839             ep.put(ExtendedProperties.NAME, name);
840             ep.put(ExtendedProperties.VALUE, value);
841             Uri result = resolver.insert(uri, ep);
842             return ContentUris.parseId(result);
843         }
844 
845         /**
846          * Finds all ExtendedProperties rows for the specified event.  The returned cursor will
847          * use {@link ExtendedPropertiesHelper#EXTENDED_PROPERTIES_PROJECTION}.
848          */
findExtendedPropertiesByEventId(ContentResolver resolver, long eventId)849         public static Cursor findExtendedPropertiesByEventId(ContentResolver resolver,
850                 long eventId) {
851             return resolver.query(ExtendedProperties.CONTENT_URI, EXTENDED_PROPERTIES_PROJECTION,
852                     ExtendedProperties.EVENT_ID + "=?",
853                     new String[] { String.valueOf(eventId) }, null);
854         }
855 
856         /**
857          * Finds an ExtendedProperties entry with a matching name for the specified event, and
858          * returns the value.  Throws an exception if we don't find exactly one row.
859          */
lookupValueByName(ContentResolver resolver, long eventId, String name)860         public static String lookupValueByName(ContentResolver resolver, long eventId,
861                 String name) {
862             Cursor cursor = resolver.query(ExtendedProperties.CONTENT_URI,
863                     EXTENDED_PROPERTIES_PROJECTION,
864                     ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?",
865                     new String[] { String.valueOf(eventId), name }, null);
866 
867             try {
868                 if (cursor.getCount() != 1) {
869                     throw new RuntimeException("Got " + cursor.getCount() + " results, expected 1");
870                 }
871 
872                 cursor.moveToFirst();
873                 return cursor.getString(EXTENDED_PROPERTIES_VALUE_INDEX);
874             } finally {
875                 if (cursor != null) {
876                     cursor.close();
877                 }
878             }
879         }
880     }
881 
882     /**
883      * Creates an updated URI that includes query parameters that identify the source as a
884      * sync adapter.
885      */
asSyncAdapter(Uri uri, String account, String accountType)886     static Uri asSyncAdapter(Uri uri, String account, String accountType) {
887         return uri.buildUpon()
888                 .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,
889                         "true")
890                 .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
891                 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
892     }
893 
894     /**
895      * Returns the value of the specified row and column in the Events table, as an integer.
896      * Throws an exception if the specified row or column doesn't exist or doesn't contain
897      * an integer (e.g. null entry).
898      */
getIntFromDatabase(ContentResolver resolver, Uri uri, long rowId, String columnName)899     private static int getIntFromDatabase(ContentResolver resolver, Uri uri, long rowId,
900             String columnName) {
901         String[] projection = { columnName };
902         String selection = SQL_WHERE_ID;
903         String[] selectionArgs = { String.valueOf(rowId) };
904 
905         Cursor c = resolver.query(uri, projection, selection, selectionArgs, null);
906         try {
907             assertEquals(1, c.getCount());
908             c.moveToFirst();
909             return c.getInt(0);
910         } finally {
911             c.close();
912         }
913     }
914 
915     @Override
setUp()916     protected void setUp() throws Exception {
917         super.setUp();
918         mContentResolver = getInstrumentation().getTargetContext().getContentResolver();
919     }
920 
921     @MediumTest
testCalendarCreationAndDeletion()922     public void testCalendarCreationAndDeletion() {
923         String account = "cc1_account";
924         int seed = 0;
925 
926         // Clean up just in case
927         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
928         long id = createAndVerifyCalendar(account, seed++, null);
929 
930         removeAndVerifyCalendar(account, id);
931     }
932 
933     /**
934      * Tests whether the default projections work.  We don't need to have any data in
935      * the calendar, since it's testing the database schema.
936      */
937     @MediumTest
testDefaultProjections()938     public void testDefaultProjections() {
939         String account = "dproj_account";
940         int seed = 0;
941 
942         // Clean up just in case
943         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
944         long id = createAndVerifyCalendar(account, seed++, null);
945 
946         Cursor c;
947         Uri uri;
948         // Calendars
949         c = mContentResolver.query(Calendars.CONTENT_URI, null, null, null, null);
950         c.close();
951         // Events
952         c = mContentResolver.query(Events.CONTENT_URI, null, null, null, null);
953         c.close();
954         // Instances
955         uri = Uri.withAppendedPath(Instances.CONTENT_URI, "0/1");
956         c = mContentResolver.query(uri, null, null, null, null);
957         c.close();
958         // Attendees
959         c = mContentResolver.query(Attendees.CONTENT_URI, null, null, null, null);
960         c.close();
961         // Reminders (only REMINDERS_ID currently uses default projection)
962         uri = ContentUris.withAppendedId(Reminders.CONTENT_URI, 0);
963         c = mContentResolver.query(uri, null, null, null, null);
964         c.close();
965         // CalendarAlerts
966         c = mContentResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI,
967                 null, null, null, null);
968         c.close();
969         // CalendarCache
970         c = mContentResolver.query(CalendarContract.CalendarCache.URI,
971                 null, null, null, null);
972         c.close();
973         // CalendarEntity
974         c = mContentResolver.query(CalendarContract.CalendarEntity.CONTENT_URI,
975                 null, null, null, null);
976         c.close();
977         // EventEntities
978         c = mContentResolver.query(CalendarContract.EventsEntity.CONTENT_URI,
979                 null, null, null, null);
980         c.close();
981         // EventDays
982         uri = Uri.withAppendedPath(CalendarContract.EventDays.CONTENT_URI, "1/2");
983         c = mContentResolver.query(uri, null, null, null, null);
984         c.close();
985         // ExtendedProperties
986         c = mContentResolver.query(CalendarContract.ExtendedProperties.CONTENT_URI,
987                 null, null, null, null);
988         c.close();
989 
990         removeAndVerifyCalendar(account, id);
991     }
992 
993     /**
994      * Exercises the EventsEntity class.
995      */
996     @MediumTest
testEventsEntityQuery()997     public void testEventsEntityQuery() {
998         String account = "eeq_account";
999         int seed = 0;
1000 
1001         // Clean up just in case.
1002         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
1003 
1004         // Create calendar.
1005         long calendarId = createAndVerifyCalendar(account, seed++, null);
1006 
1007         // Create three events.  We need to make sure SELF_ATTENDEE_STATUS isn't set, because
1008         // that causes the provider to generate an Attendees entry, and that'll throw off
1009         // our expected count.
1010         ContentValues eventValues;
1011         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
1012         eventValues.remove(Events.SELF_ATTENDEE_STATUS);
1013         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
1014         assertTrue(eventId1 >= 0);
1015 
1016         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
1017         eventValues.remove(Events.SELF_ATTENDEE_STATUS);
1018         long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
1019         assertTrue(eventId2 >= 0);
1020 
1021         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
1022         eventValues.remove(Events.SELF_ATTENDEE_STATUS);
1023         long eventId3 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
1024         assertTrue(eventId3 >= 0);
1025 
1026         /*
1027          * Add some attendees, reminders, and extended properties.
1028          */
1029         Uri uri, syncUri;
1030 
1031         syncUri = asSyncAdapter(Reminders.CONTENT_URI, account, CTS_TEST_TYPE);
1032         ContentValues remValues = new ContentValues();
1033         remValues.put(Reminders.EVENT_ID, eventId1);
1034         remValues.put(Reminders.MINUTES, 10);
1035         remValues.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1036         mContentResolver.insert(syncUri, remValues);
1037         remValues.put(Reminders.MINUTES, 20);
1038         mContentResolver.insert(syncUri, remValues);
1039 
1040         syncUri = asSyncAdapter(ExtendedProperties.CONTENT_URI, account, CTS_TEST_TYPE);
1041         ContentValues extended = new ContentValues();
1042         extended.put(ExtendedProperties.NAME, "foo");
1043         extended.put(ExtendedProperties.VALUE, "bar");
1044         extended.put(ExtendedProperties.EVENT_ID, eventId2);
1045         mContentResolver.insert(syncUri, extended);
1046         extended.put(ExtendedProperties.EVENT_ID, eventId1);
1047         mContentResolver.insert(syncUri, extended);
1048         extended.put(ExtendedProperties.NAME, "foo2");
1049         extended.put(ExtendedProperties.VALUE, "bar2");
1050         mContentResolver.insert(syncUri, extended);
1051 
1052         syncUri = asSyncAdapter(Attendees.CONTENT_URI, account, CTS_TEST_TYPE);
1053         ContentValues attendee = new ContentValues();
1054         attendee.put(Attendees.ATTENDEE_NAME, "Joe");
1055         attendee.put(Attendees.ATTENDEE_EMAIL, CalendarHelper.generateCalendarOwnerEmail(account));
1056         attendee.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED);
1057         attendee.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
1058         attendee.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_PERFORMER);
1059         attendee.put(Attendees.EVENT_ID, eventId3);
1060         mContentResolver.insert(syncUri, attendee);
1061 
1062         /*
1063          * Iterate over all events on our calendar.  Peek at a few values to see if they
1064          * look reasonable.
1065          */
1066         EntityIterator ei = EventsEntity.newEntityIterator(
1067                 mContentResolver.query(EventsEntity.CONTENT_URI, null, Events.CALENDAR_ID + "=?",
1068                         new String[] { String.valueOf(calendarId) }, null),
1069                 mContentResolver);
1070         int count = 0;
1071         try {
1072             while (ei.hasNext()) {
1073                 Entity entity = ei.next();
1074                 ContentValues values = entity.getEntityValues();
1075                 ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues();
1076                 long eventId = values.getAsLong(Events._ID);
1077                 if (eventId == eventId1) {
1078                     // 2 x reminder, 2 x extended properties
1079                     assertEquals(4, subvalues.size());
1080                 } else if (eventId == eventId2) {
1081                     // Extended properties
1082                     assertEquals(1, subvalues.size());
1083                     ContentValues subContentValues = subvalues.get(0).values;
1084                     String name = subContentValues.getAsString(
1085                             CalendarContract.ExtendedProperties.NAME);
1086                     String value = subContentValues.getAsString(
1087                             CalendarContract.ExtendedProperties.VALUE);
1088                     assertEquals("foo", name);
1089                     assertEquals("bar", value);
1090                 } else if (eventId == eventId3) {
1091                     // Attendees
1092                     assertEquals(1, subvalues.size());
1093                 } else {
1094                     fail("should not be here");
1095                 }
1096                 count++;
1097             }
1098             assertEquals(3, count);
1099         } finally {
1100             ei.close();
1101         }
1102 
1103         // Confirm that querying for a single event yields a single event.
1104         ei = EventsEntity.newEntityIterator(
1105                 mContentResolver.query(EventsEntity.CONTENT_URI, null, SQL_WHERE_ID,
1106                         new String[] { String.valueOf(eventId3) }, null),
1107                 mContentResolver);
1108         try {
1109             count = 0;
1110             while (ei.hasNext()) {
1111                 Entity entity = ei.next();
1112                 count++;
1113             }
1114             assertEquals(1, count);
1115         } finally {
1116             ei.close();
1117         }
1118 
1119 
1120         removeAndVerifyCalendar(account, calendarId);
1121     }
1122 
1123     /**
1124      * Exercises the CalendarEntity class.
1125      */
1126     @MediumTest
testCalendarEntityQuery()1127     public void testCalendarEntityQuery() {
1128         String account1 = "ceq1_account";
1129         String account2 = "ceq2_account";
1130         String account3 = "ceq3_account";
1131         int seed = 0;
1132 
1133         // Clean up just in case.
1134         CalendarHelper.deleteCalendarByAccount(mContentResolver, account1);
1135         CalendarHelper.deleteCalendarByAccount(mContentResolver, account2);
1136         CalendarHelper.deleteCalendarByAccount(mContentResolver, account3);
1137 
1138         // Create calendars.
1139         long calendarId1 = createAndVerifyCalendar(account1, seed++, null);
1140         long calendarId2 = createAndVerifyCalendar(account2, seed++, null);
1141         long calendarId3 = createAndVerifyCalendar(account3, seed++, null);
1142 
1143         EntityIterator ei = CalendarEntity.newEntityIterator(
1144                 mContentResolver.query(CalendarEntity.CONTENT_URI, null,
1145                         Calendars._ID + "=? OR " + Calendars._ID + "=? OR " + Calendars._ID + "=?",
1146                         new String[] { String.valueOf(calendarId1), String.valueOf(calendarId2),
1147                                 String.valueOf(calendarId3) },
1148                         null));
1149 
1150         try {
1151             int count = 0;
1152             while (ei.hasNext()) {
1153                 Entity entity = ei.next();
1154                 count++;
1155             }
1156             assertEquals(3, count);
1157         } finally {
1158             ei.close();
1159         }
1160 
1161         removeAndVerifyCalendar(account1, calendarId1);
1162         removeAndVerifyCalendar(account2, calendarId2);
1163         removeAndVerifyCalendar(account3, calendarId3);
1164     }
1165 
1166     /**
1167      * Tests creation and manipulation of Attendees.
1168      */
1169     @MediumTest
testAttendees()1170     public void testAttendees() {
1171         String account = "att_account";
1172         int seed = 0;
1173 
1174         // Clean up just in case.
1175         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
1176 
1177         // Create calendar.
1178         long calendarId = createAndVerifyCalendar(account, seed++, null);
1179 
1180         // Create two events, one with a value set for SELF_ATTENDEE_STATUS, one without.
1181         ContentValues eventValues;
1182         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
1183         eventValues.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE);
1184         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
1185         assertTrue(eventId1 >= 0);
1186 
1187         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
1188         eventValues.remove(Events.SELF_ATTENDEE_STATUS);
1189         long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
1190         assertTrue(eventId2 >= 0);
1191 
1192         /*
1193          * Add some attendees to the first event.
1194          */
1195         long attId1 = AttendeeHelper.addAttendee(mContentResolver, eventId1,
1196                 "Alice",
1197                 "alice@example.com",
1198                 Attendees.ATTENDEE_STATUS_TENTATIVE,
1199                 Attendees.RELATIONSHIP_ATTENDEE,
1200                 Attendees.TYPE_REQUIRED);
1201         long attId2 = AttendeeHelper.addAttendee(mContentResolver, eventId1,
1202                 "Betty",
1203                 "betty@example.com",
1204                 Attendees.ATTENDEE_STATUS_DECLINED,
1205                 Attendees.RELATIONSHIP_ATTENDEE,
1206                 Attendees.TYPE_NONE);
1207         long attId3 = AttendeeHelper.addAttendee(mContentResolver, eventId1,
1208                 "Carol",
1209                 "carol@example.com",
1210                 Attendees.ATTENDEE_STATUS_DECLINED,
1211                 Attendees.RELATIONSHIP_ATTENDEE,
1212                 Attendees.TYPE_OPTIONAL);
1213 
1214         /*
1215          * Find the event1 "self" attendee entry.
1216          */
1217         Cursor cursor = AttendeeHelper.findAttendeesByEmail(mContentResolver, eventId1,
1218                 CalendarHelper.generateCalendarOwnerEmail(account));
1219         try {
1220             assertEquals(1, cursor.getCount());
1221             //DatabaseUtils.dumpCursor(cursor);
1222 
1223             cursor.moveToFirst();
1224             long id = cursor.getLong(AttendeeHelper.ATTENDEES_ID_INDEX);
1225 
1226             /*
1227              * Update the status field.  The provider should automatically propagate the result.
1228              */
1229             ContentValues update = new ContentValues();
1230             Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, id);
1231 
1232             update.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
1233             int count = mContentResolver.update(uri, update, null, null);
1234             assertEquals(1, count);
1235 
1236             int status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId1);
1237             assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, status);
1238 
1239         } finally {
1240             if (cursor != null) {
1241                 cursor.close();
1242             }
1243         }
1244 
1245         /*
1246          * Do a bulk update of all Attendees for this event, changing any Attendee with status
1247          * "declined" to "invited".
1248          */
1249         ContentValues bulkUpdate = new ContentValues();
1250         bulkUpdate.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_INVITED);
1251 
1252         int count = mContentResolver.update(Attendees.CONTENT_URI, bulkUpdate,
1253                 Attendees.EVENT_ID + "=? AND " + Attendees.ATTENDEE_STATUS + "=?",
1254                 new String[] {
1255                     String.valueOf(eventId1), String.valueOf(Attendees.ATTENDEE_STATUS_DECLINED)
1256                 });
1257         assertEquals(2, count);
1258 
1259         /*
1260          * Add a new, non-self attendee to the second event.
1261          */
1262         long attId4 = AttendeeHelper.addAttendee(mContentResolver, eventId2,
1263                 "Diana",
1264                 "diana@example.com",
1265                 Attendees.ATTENDEE_STATUS_ACCEPTED,
1266                 Attendees.RELATIONSHIP_ATTENDEE,
1267                 Attendees.TYPE_REQUIRED);
1268 
1269         /*
1270          * Confirm that the selfAttendeeStatus on the second event has the default value.
1271          */
1272         int status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId2);
1273         assertEquals(Attendees.ATTENDEE_STATUS_NONE, status);
1274 
1275         /*
1276          * Create a new "self" attendee in the second event by updating the email address to
1277          * match that of the calendar owner.
1278          */
1279         ContentValues newSelf = new ContentValues();
1280         newSelf.put(Attendees.ATTENDEE_EMAIL, CalendarHelper.generateCalendarOwnerEmail(account));
1281         count = mContentResolver.update(ContentUris.withAppendedId(Attendees.CONTENT_URI, attId4),
1282                 newSelf, null, null);
1283         assertEquals(1, count);
1284 
1285         /*
1286          * Confirm that the event's selfAttendeeStatus has been updated.
1287          */
1288         status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId2);
1289         assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, status);
1290 
1291         /*
1292          * TODO:  (these are unexpected usage patterns)
1293          * - Update an Attendee's status and event_id to move it to a different event, and
1294          *   confirm that the selfAttendeeStatus in the destination event is updated (rather
1295          *   than that of the source event).
1296          * - Create two Attendees with email addresses that match "self" but have different
1297          *   values for "status".  Delete one and confirm that selfAttendeeStatus is changed
1298          *   to that of the remaining Attendee.  (There is no defined behavior for
1299          *   selfAttendeeStatus when there are multiple matching Attendees.)
1300          */
1301 
1302         /*
1303          * Test deletion, singly by ID and in bulk.
1304          */
1305         count = mContentResolver.delete(ContentUris.withAppendedId(Attendees.CONTENT_URI, attId4),
1306                 null, null);
1307         assertEquals(1, count);
1308 
1309         count = mContentResolver.delete(Attendees.CONTENT_URI, Attendees.EVENT_ID + "=?",
1310                 new String[] { String.valueOf(eventId1) });
1311         assertEquals(4, count);     // 3 we created + 1 auto-added by the provider
1312 
1313         removeAndVerifyCalendar(account, calendarId);
1314     }
1315 
1316     /**
1317      * Tests creation and manipulation of Reminders.
1318      */
1319     @MediumTest
testReminders()1320     public void testReminders() {
1321         String account = "rem_account";
1322         int seed = 0;
1323 
1324         // Clean up just in case.
1325         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
1326 
1327         // Create calendar.
1328         long calendarId = createAndVerifyCalendar(account, seed++, null);
1329 
1330         // Create events.
1331         ContentValues eventValues;
1332         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
1333         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
1334         assertTrue(eventId1 >= 0);
1335         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
1336         long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
1337         assertTrue(eventId2 >= 0);
1338 
1339         // No reminders, hasAlarm should be zero.
1340         int hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1);
1341         assertEquals(0, hasAlarm);
1342         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2);
1343         assertEquals(0, hasAlarm);
1344 
1345         /*
1346          * Add some reminders.
1347          */
1348         long remId1 = ReminderHelper.addReminder(mContentResolver, eventId1,
1349                 10, Reminders.METHOD_DEFAULT);
1350         long remId2 = ReminderHelper.addReminder(mContentResolver, eventId1,
1351                 15, Reminders.METHOD_ALERT);
1352         long remId3 = ReminderHelper.addReminder(mContentResolver, eventId1,
1353                 20, Reminders.METHOD_SMS);  // SMS isn't allowed for this calendar
1354 
1355         // Should have been set to 1 by provider.
1356         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1);
1357         assertEquals(1, hasAlarm);
1358 
1359         // Add a reminder to event2.
1360         ReminderHelper.addReminder(mContentResolver, eventId2,
1361                 20, Reminders.METHOD_DEFAULT);
1362         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2);
1363         assertEquals(1, hasAlarm);
1364 
1365 
1366         /*
1367          * Check the entries.
1368          */
1369         Cursor cursor = ReminderHelper.findRemindersByEventId(mContentResolver, eventId1);
1370         try {
1371             assertEquals(3, cursor.getCount());
1372             //DatabaseUtils.dumpCursor(cursor);
1373 
1374             while (cursor.moveToNext()) {
1375                 int minutes = cursor.getInt(ReminderHelper.REMINDERS_MINUTES_INDEX);
1376                 int method = cursor.getInt(ReminderHelper.REMINDERS_METHOD_INDEX);
1377                 switch (minutes) {
1378                     case 10:
1379                         assertEquals(Reminders.METHOD_DEFAULT, method);
1380                         break;
1381                     case 15:
1382                         assertEquals(Reminders.METHOD_ALERT, method);
1383                         break;
1384                     case 20:
1385                         assertEquals(Reminders.METHOD_SMS, method);
1386                         break;
1387                     default:
1388                         fail("unexpected minutes " + minutes);
1389                         break;
1390                 }
1391             }
1392         } finally {
1393             if (cursor != null) {
1394                 cursor.close();
1395             }
1396         }
1397 
1398         /*
1399          * Use the bulk update feature to change all METHOD_DEFAULT to METHOD_EMAIL.  To make
1400          * this more interesting we first change remId3 to METHOD_DEFAULT.
1401          */
1402         int count;
1403         ContentValues newValues = new ContentValues();
1404         newValues.put(Reminders.METHOD, Reminders.METHOD_DEFAULT);
1405         count = mContentResolver.update(ContentUris.withAppendedId(Reminders.CONTENT_URI, remId3),
1406                 newValues, null, null);
1407         assertEquals(1, count);
1408 
1409         newValues.put(Reminders.METHOD, Reminders.METHOD_EMAIL);
1410         count = mContentResolver.update(Reminders.CONTENT_URI, newValues,
1411                 Reminders.EVENT_ID + "=? AND " + Reminders.METHOD + "=?",
1412                 new String[] {
1413                     String.valueOf(eventId1), String.valueOf(Reminders.METHOD_DEFAULT)
1414                 });
1415         assertEquals(2, count);
1416 
1417         // check it
1418         int method = ReminderHelper.lookupMethod(mContentResolver, remId3);
1419         assertEquals(Reminders.METHOD_EMAIL, method);
1420 
1421         /*
1422          * Delete some / all reminders and confirm that hasAlarm tracks it.
1423          *
1424          * You can also remove reminders from an event by updating the event_id column, but
1425          * that's defined as producing undefined behavior, so we don't do it here.
1426          */
1427         count = mContentResolver.delete(Reminders.CONTENT_URI,
1428                 Reminders.EVENT_ID + "=? AND " + Reminders.MINUTES + ">=?",
1429                 new String[] { String.valueOf(eventId1), "15" });
1430         assertEquals(2, count);
1431         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1);
1432         assertEquals(1, hasAlarm);
1433 
1434         // Delete all reminders from both events.
1435         count = mContentResolver.delete(Reminders.CONTENT_URI,
1436                 Reminders.EVENT_ID + "=? OR " + Reminders.EVENT_ID + "=?",
1437                 new String[] { String.valueOf(eventId1), String.valueOf(eventId2) });
1438         assertEquals(2, count);
1439         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1);
1440         assertEquals(0, hasAlarm);
1441         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2);
1442         assertEquals(0, hasAlarm);
1443 
1444         /*
1445          * Add a couple of reminders and then delete one with the by-ID URI.
1446          */
1447         long remId4 = ReminderHelper.addReminder(mContentResolver, eventId1,
1448                 10, Reminders.METHOD_EMAIL);
1449         long remId5 = ReminderHelper.addReminder(mContentResolver, eventId1,
1450                 15, Reminders.METHOD_EMAIL);
1451         count = mContentResolver.delete(ContentUris.withAppendedId(Reminders.CONTENT_URI, remId4),
1452                 null, null);
1453         assertEquals(1, count);
1454 
1455         removeAndVerifyCalendar(account, calendarId);
1456     }
1457 
1458     /**
1459      * A listener for the EVENT_REMINDER broadcast that is expected to be fired by the
1460      * provider at the reminder time.
1461      */
1462     public class MockReminderReceiver extends BroadcastReceiver {
1463         public boolean received = false;
1464 
1465         @Override
onReceive(Context context, Intent intent)1466         public void onReceive(Context context, Intent intent) {
1467             final String action = intent.getAction();
1468             if (action.equals(CalendarContract.ACTION_EVENT_REMINDER)) {
1469                 received = true;
1470             }
1471         }
1472     }
1473 
1474     /**
1475      * Test that reminders result in the expected broadcast at reminder time.
1476      */
testRemindersAlarm()1477     public void testRemindersAlarm() throws Exception {
1478         // Setup: register a mock listener for the broadcast we expect to fire at the
1479         // reminder time.
1480         final MockReminderReceiver reminderReceiver = new MockReminderReceiver();
1481         IntentFilter filter = new IntentFilter(CalendarContract.ACTION_EVENT_REMINDER);
1482         filter.addDataScheme("content");
1483         getInstrumentation().getTargetContext().registerReceiver(reminderReceiver, filter);
1484 
1485         // Clean up just in case.
1486         String account = "rem_account";
1487         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
1488 
1489         // Create calendar.  Use '1' as seed as this sets the VISIBLE field to 1.
1490         // The calendar must be visible for its notifications to occur.
1491         long calendarId = createAndVerifyCalendar(account, 1, null);
1492 
1493         // Create event for 15 min in the past, with a 10 min reminder, so that it will
1494         // trigger immediately.
1495         ContentValues eventValues;
1496         int seed = 0;
1497         long now = System.currentTimeMillis();
1498         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
1499         eventValues.put(Events.DTSTART, now - DateUtils.MINUTE_IN_MILLIS * 15);
1500         eventValues.put(Events.DTEND, now + DateUtils.HOUR_IN_MILLIS);
1501         long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
1502         assertTrue(eventId >= 0);
1503         ReminderHelper.addReminder(mContentResolver, eventId, 10, Reminders.METHOD_ALERT);
1504 
1505         // Confirm that the EVENT_REMINDER broadcast was fired by the provider.
1506         new PollingCheck(POLLING_TIMEOUT) {
1507             @Override
1508             protected boolean check() {
1509                 return reminderReceiver.received;
1510             }
1511         }.run();
1512         assertTrue(reminderReceiver.received);
1513 
1514         removeAndVerifyCalendar(account, calendarId);
1515     }
1516 
1517     @MediumTest
testColorWriteRequirements()1518     public void testColorWriteRequirements() {
1519         String account = "colw_account";
1520         String account2 = "colw2_account";
1521         int seed = 0;
1522         Uri uri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE);
1523         Uri uri2 = asSyncAdapter(Colors.CONTENT_URI, account2, CTS_TEST_TYPE);
1524 
1525         // Clean up just in case
1526         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
1527         ColorHelper.deleteColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE);
1528 
1529         ContentValues colorValues = new ContentValues();
1530         // Account name/type must be in the query params, so may be left
1531         // out here
1532         colorValues.put(Colors.DATA, "0");
1533         colorValues.put(Colors.COLOR_KEY, "1");
1534         colorValues.put(Colors.COLOR_TYPE, 0);
1535         colorValues.put(Colors.COLOR, 0xff000000);
1536 
1537         // Verify only a sync adapter can write to Colors
1538         try {
1539             mContentResolver.insert(Colors.CONTENT_URI, colorValues);
1540             fail("Should not allow non-sync adapter to insert colors");
1541         } catch (IllegalArgumentException e) {
1542             // WAI
1543         }
1544 
1545         // Verify everything except DATA is required
1546         ContentValues testVals = new ContentValues(colorValues);
1547         for (String key : colorValues.keySet()) {
1548 
1549             testVals.remove(key);
1550             try {
1551                 Uri colUri = mContentResolver.insert(uri, testVals);
1552                 if (!TextUtils.equals(key, Colors.DATA)) {
1553                     // The DATA field is allowed to be empty.
1554                     fail("Should not allow color creation without " + key);
1555                 }
1556                 ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
1557             } catch (IllegalArgumentException e) {
1558                 if (TextUtils.equals(key, Colors.DATA)) {
1559                     // The DATA field is allowed to be empty.
1560                     fail("Should allow color creation without " + key);
1561                 }
1562             }
1563             testVals.put(key, colorValues.getAsString(key));
1564         }
1565 
1566         // Verify writing a color works
1567         Uri col1 = mContentResolver.insert(uri, colorValues);
1568 
1569         // Verify adding the same color fails
1570         try {
1571             mContentResolver.insert(uri, colorValues);
1572             fail("Should not allow adding the same color twice");
1573         } catch (IllegalArgumentException e) {
1574             // WAI
1575         }
1576 
1577         // Verify specifying a different account than the query params doesn't work
1578         colorValues.put(Colors.ACCOUNT_NAME, account2);
1579         try {
1580             mContentResolver.insert(uri, colorValues);
1581             fail("Should use the account from the query params, not the values.");
1582         } catch (IllegalArgumentException e) {
1583             // WAI
1584         }
1585 
1586         // Verify adding a color to a different account works
1587         Uri col2 = mContentResolver.insert(uri2, colorValues);
1588 
1589         // And a different index on the same account
1590         colorValues.put(Colors.COLOR_KEY, "2");
1591         Uri col3 = mContentResolver.insert(uri2, colorValues);
1592 
1593         // Verify that all three colors are in the table
1594         Cursor c = ColorHelper.findColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
1595         assertEquals(1, c.getCount());
1596         c.close();
1597         c = ColorHelper.findColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE);
1598         assertEquals(2, c.getCount());
1599         c.close();
1600 
1601         // Verify deleting them works
1602         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
1603         ColorHelper.deleteColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE);
1604 
1605         c = ColorHelper.findColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
1606         assertEquals(0, c.getCount());
1607         c.close();
1608         c = ColorHelper.findColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE);
1609         assertEquals(0, c.getCount());
1610         c.close();
1611     }
1612 
1613     /**
1614      * Tests Colors interaction with the Calendars table.
1615      */
1616     @MediumTest
testCalendarColors()1617     public void testCalendarColors() {
1618         String account = "cc_account";
1619         int seed = 0;
1620 
1621         // Clean up just in case
1622         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
1623         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
1624 
1625         // Test inserting a calendar with an invalid color index
1626         ContentValues cv = CalendarHelper.getNewCalendarValues(account, seed++);
1627         cv.put(Calendars.CALENDAR_COLOR_KEY, "badIndex");
1628         Uri calSyncUri = asSyncAdapter(Calendars.CONTENT_URI, account, CTS_TEST_TYPE);
1629         Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE);
1630 
1631         try {
1632             Uri uri = mContentResolver.insert(calSyncUri, cv);
1633             fail("Should not allow insertion of invalid color index into Calendars");
1634         } catch (IllegalArgumentException e) {
1635             // WAI
1636         }
1637 
1638         // Test updating a calendar with an invalid color index
1639         long calendarId = createAndVerifyCalendar(account, seed++, null);
1640         cv.clear();
1641         cv.put(Calendars.CALENDAR_COLOR_KEY, "badIndex2");
1642         Uri calendarUri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calendarId);
1643         try {
1644             mContentResolver.update(calendarUri, cv, null, null);
1645             fail("Should not allow update of invalid color index into Calendars");
1646         } catch (IllegalArgumentException e) {
1647             // WAI
1648         }
1649 
1650         assertTrue(ColorHelper.addDefaultColorsToAccount(mContentResolver, account, CTS_TEST_TYPE));
1651 
1652         // Test that inserting a valid color index works
1653         cv = CalendarHelper.getNewCalendarValues(account, seed++);
1654         cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]);
1655 
1656         Uri uri = mContentResolver.insert(calSyncUri, cv);
1657         long calendarId2 = ContentUris.parseId(uri);
1658         assertTrue(calendarId2 >= 0);
1659         // And updates the calendar's color to the one in the table
1660         cv.put(Calendars.CALENDAR_COLOR, ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0]);
1661         verifyCalendar(account, cv, calendarId2, 2);
1662 
1663         // Test that updating a valid color index also updates the color in a
1664         // calendar
1665         cv.clear();
1666         cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]);
1667         mContentResolver.update(calendarUri, cv, null, null);
1668         Cursor c = mContentResolver.query(calendarUri,
1669                 new String[] { Calendars.CALENDAR_COLOR_KEY, Calendars.CALENDAR_COLOR },
1670                 null, null, null);
1671         try {
1672             c.moveToFirst();
1673             String index = c.getString(0);
1674             int color = c.getInt(1);
1675             assertEquals(index, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]);
1676             assertEquals(color, ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0]);
1677         } finally {
1678             if (c != null) {
1679                 c.close();
1680             }
1681         }
1682 
1683         // And clearing it doesn't change the color
1684         cv.put(Calendars.CALENDAR_COLOR_KEY, (String) null);
1685         mContentResolver.update(calendarUri, cv, null, null);
1686         c = mContentResolver.query(calendarUri,
1687                 new String[] { Calendars.CALENDAR_COLOR_KEY, Calendars.CALENDAR_COLOR },
1688                 null, null, null);
1689         try {
1690             c.moveToFirst();
1691             String index = c.getString(0);
1692             int color = c.getInt(1);
1693             assertEquals(index, null);
1694             assertEquals(ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0], color);
1695         } finally {
1696             if (c != null) {
1697                 c.close();
1698             }
1699         }
1700 
1701         // Test that setting a calendar color to an event color fails
1702         cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_0]);
1703         try {
1704             mContentResolver.update(calendarUri, cv, null, null);
1705             fail("Should not allow a calendar to use an event color");
1706         } catch (IllegalArgumentException e) {
1707             // WAI
1708         }
1709 
1710         // Test that you can't remove a color that is referenced by a calendar
1711         cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_3]);
1712         mContentResolver.update(calendarUri, cv, null, null);
1713 
1714         try {
1715             mContentResolver.delete(colSyncUri, ColorHelper.WHERE_COLOR_ACCOUNT_AND_INDEX,
1716                     new String[] {
1717                             account, CTS_TEST_TYPE,
1718                             ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_3]
1719                     });
1720             fail("Should not allow deleting referenced color");
1721         } catch (UnsupportedOperationException e) {
1722             // WAI
1723         }
1724 
1725         // Clean up
1726         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
1727         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
1728     }
1729 
1730     /**
1731      * Tests Colors interaction with the Events table.
1732      */
1733     @MediumTest
testEventColors()1734     public void testEventColors() {
1735         String account = "ec_account";
1736         int seed = 0;
1737 
1738         // Clean up just in case
1739         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
1740         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
1741 
1742         // Test inserting an event with an invalid color index
1743         long cal_id = createAndVerifyCalendar(account, seed++, null);
1744 
1745         Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE);
1746 
1747         ContentValues ev = EventHelper.getNewEventValues(account, seed++, cal_id, false);
1748         ev.put(Events.EVENT_COLOR_KEY, "badIndex");
1749 
1750         try {
1751             Uri uri = mContentResolver.insert(Events.CONTENT_URI, ev);
1752             fail("Should not allow insertion of invalid color index into Events");
1753         } catch (IllegalArgumentException e) {
1754             // WAI
1755         }
1756 
1757         // Test updating an event with an invalid color index fails
1758         long event_id = createAndVerifyEvent(account, seed++, cal_id, false, null);
1759         ev.clear();
1760         ev.put(Events.EVENT_COLOR_KEY, "badIndex2");
1761         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, event_id);
1762         try {
1763             mContentResolver.update(eventUri, ev, null, null);
1764             fail("Should not allow update of invalid color index into Events");
1765         } catch (IllegalArgumentException e) {
1766             // WAI
1767         }
1768 
1769         assertTrue(ColorHelper.addDefaultColorsToAccount(mContentResolver, account, CTS_TEST_TYPE));
1770 
1771         // Test that inserting a valid color index works
1772         ev = EventHelper.getNewEventValues(account, seed++, cal_id, false);
1773         final String defaultColorIndex = ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_0];
1774         ev.put(Events.EVENT_COLOR_KEY, defaultColorIndex);
1775 
1776         Uri uri = mContentResolver.insert(Events.CONTENT_URI, ev);
1777         long eventId2 = ContentUris.parseId(uri);
1778         assertTrue(eventId2 >= 0);
1779         // And updates the event's color to the one in the table
1780         final int expectedColor = ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_0];
1781         ev.put(Events.EVENT_COLOR, expectedColor);
1782         verifyEvent(ev, eventId2);
1783 
1784         // Test that event iterator has COLOR columns
1785         final EntityIterator iterator = EventsEntity.newEntityIterator(mContentResolver.query(
1786                 ContentUris.withAppendedId(EventsEntity.CONTENT_URI, eventId2),
1787                 null, null, null, null), mContentResolver);
1788         assertTrue("Empty Iterator", iterator.hasNext());
1789         final Entity entity = iterator.next();
1790         final ContentValues values = entity.getEntityValues();
1791         assertTrue("Missing EVENT_COLOR", values.containsKey(EventsEntity.EVENT_COLOR));
1792         assertEquals("Wrong EVENT_COLOR",
1793                 expectedColor,
1794                 (int) values.getAsInteger(EventsEntity.EVENT_COLOR));
1795         assertTrue("Missing EVENT_COLOR_KEY", values.containsKey(EventsEntity.EVENT_COLOR_KEY));
1796         assertEquals("Wrong EVENT_COLOR_KEY",
1797                 defaultColorIndex,
1798                 values.getAsString(EventsEntity.EVENT_COLOR_KEY));
1799         iterator.close();
1800 
1801         // Test that updating a valid color index also updates the color in an
1802         // event
1803         ev.clear();
1804         ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]);
1805         mContentResolver.update(eventUri, ev, null, null);
1806         Cursor c = mContentResolver.query(eventUri, new String[] {
1807                 Events.EVENT_COLOR_KEY, Events.EVENT_COLOR
1808         }, null, null, null);
1809         try {
1810             c.moveToFirst();
1811             String index = c.getString(0);
1812             int color = c.getInt(1);
1813             assertEquals(index, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]);
1814             assertEquals(color, ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_1]);
1815         } finally {
1816             if (c != null) {
1817                 c.close();
1818             }
1819         }
1820 
1821         // And clearing it doesn't change the color
1822         ev.put(Events.EVENT_COLOR_KEY, (String) null);
1823         mContentResolver.update(eventUri, ev, null, null);
1824         c = mContentResolver.query(eventUri, new String[] {
1825                 Events.EVENT_COLOR_KEY, Events.EVENT_COLOR
1826         }, null, null, null);
1827         try {
1828             c.moveToFirst();
1829             String index = c.getString(0);
1830             int color = c.getInt(1);
1831             assertEquals(index, null);
1832             assertEquals(ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_1], color);
1833         } finally {
1834             if (c != null) {
1835                 c.close();
1836             }
1837         }
1838 
1839         // Test that setting an event color to a calendar color fails
1840         ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_2]);
1841         try {
1842             mContentResolver.update(eventUri, ev, null, null);
1843             fail("Should not allow an event to use a calendar color");
1844         } catch (IllegalArgumentException e) {
1845             // WAI
1846         }
1847 
1848         // Test that you can't remove a color that is referenced by an event
1849         ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]);
1850         mContentResolver.update(eventUri, ev, null, null);
1851         try {
1852             mContentResolver.delete(colSyncUri, ColorHelper.WHERE_COLOR_ACCOUNT_AND_INDEX,
1853                     new String[] {
1854                             account, CTS_TEST_TYPE,
1855                             ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]
1856                     });
1857             fail("Should not allow deleting referenced color");
1858         } catch (UnsupportedOperationException e) {
1859             // WAI
1860         }
1861 
1862         // TODO test colors with exceptions
1863 
1864         // Clean up
1865         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
1866         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
1867     }
1868 
1869     /**
1870      * Tests creation and manipulation of ExtendedProperties.
1871      */
1872     @MediumTest
testExtendedProperties()1873     public void testExtendedProperties() {
1874         String account = "ep_account";
1875         int seed = 0;
1876 
1877         // Clean up just in case.
1878         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
1879 
1880         // Create calendar.
1881         long calendarId = createAndVerifyCalendar(account, seed++, null);
1882 
1883         // Create events.
1884         ContentValues eventValues;
1885         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
1886         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
1887         assertTrue(eventId1 >= 0);
1888 
1889         /*
1890          * Add some extended properties.
1891          */
1892         long epId1 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account,
1893                 eventId1, "first", "Jeffrey");
1894         long epId2 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account,
1895                 eventId1, "last", "Lebowski");
1896         long epId3 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account,
1897                 eventId1, "title", "Dude");
1898 
1899         /*
1900          * Spot-check a couple of entries.
1901          */
1902         Cursor cursor = ExtendedPropertiesHelper.findExtendedPropertiesByEventId(mContentResolver,
1903                 eventId1);
1904         try {
1905             assertEquals(3, cursor.getCount());
1906             //DatabaseUtils.dumpCursor(cursor);
1907 
1908             while (cursor.moveToNext()) {
1909                 String name =
1910                     cursor.getString(ExtendedPropertiesHelper.EXTENDED_PROPERTIES_NAME_INDEX);
1911                 String value =
1912                     cursor.getString(ExtendedPropertiesHelper.EXTENDED_PROPERTIES_VALUE_INDEX);
1913 
1914                 if (name.equals("last")) {
1915                     assertEquals("Lebowski", value);
1916                 }
1917             }
1918 
1919             String title = ExtendedPropertiesHelper.lookupValueByName(mContentResolver, eventId1,
1920                     "title");
1921             assertEquals("Dude", title);
1922         } finally {
1923             if (cursor != null) {
1924                 cursor.close();
1925             }
1926         }
1927 
1928         // Update the title.  Must be done as a sync adapter.
1929         ContentValues newValues = new ContentValues();
1930         newValues.put(ExtendedProperties.VALUE, "Big");
1931         Uri uri = ContentUris.withAppendedId(ExtendedProperties.CONTENT_URI, epId3);
1932         uri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
1933         int count = mContentResolver.update(uri, newValues, null, null);
1934         assertEquals(1, count);
1935 
1936         // check it
1937         String title = ExtendedPropertiesHelper.lookupValueByName(mContentResolver, eventId1,
1938                 "title");
1939         assertEquals("Big", title);
1940 
1941         removeAndVerifyCalendar(account, calendarId);
1942     }
1943 
1944     private class CalendarEventHelper {
1945 
1946       private long mCalendarId;
1947       private String mAccount;
1948       private int mSeed;
1949 
CalendarEventHelper(String account, int seed)1950       public CalendarEventHelper(String account, int seed) {
1951         mAccount = account;
1952         mSeed = seed;
1953         ContentValues values = CalendarHelper.getNewCalendarValues(account, seed);
1954         mCalendarId = createAndVerifyCalendar(account, seed++, values);
1955       }
1956 
addEvent(String timeString, int timeZoneIndex, long duration)1957       public ContentValues addEvent(String timeString, int timeZoneIndex, long duration) {
1958         long event1Start = timeInMillis(timeString, timeZoneIndex);
1959         ContentValues eventValues;
1960         eventValues = EventHelper.getNewEventValues(mAccount, mSeed++, mCalendarId, true);
1961         eventValues.put(Events.DESCRIPTION, timeString);
1962         eventValues.put(Events.DTSTART, event1Start);
1963         eventValues.put(Events.DTEND, event1Start + duration);
1964         eventValues.put(Events.EVENT_TIMEZONE, TIME_ZONES[timeZoneIndex]);
1965         long eventId = createAndVerifyEvent(mAccount, mSeed, mCalendarId, true, eventValues);
1966         assertTrue(eventId >= 0);
1967         return eventValues;
1968       }
1969 
getCalendarId()1970       public long getCalendarId() {
1971         return mCalendarId;
1972       }
1973     }
1974 
1975     /**
1976      * Test query to retrieve instances within a certain time interval.
1977      */
testWhenByDayQuery()1978     public void testWhenByDayQuery() {
1979       String account = "cser_account";
1980       int seed = 0;
1981 
1982       // Clean up just in case
1983       CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
1984 
1985       // Create a calendar
1986       CalendarEventHelper helper = new CalendarEventHelper(account, seed);
1987 
1988       // Add events to the calendar--the first two in the queried range
1989       List<ContentValues> eventsWithinRange = new ArrayList<ContentValues>();
1990 
1991       ContentValues values = helper.addEvent("2009-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS);
1992       eventsWithinRange.add(values);
1993 
1994       values = helper.addEvent("2010-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS);
1995       eventsWithinRange.add(values);
1996 
1997       helper.addEvent("2011-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS);
1998 
1999       // Prepare the start time and end time of the range to query
2000       String startTime = "2009-01-01T00:00:00";
2001       String endTime = "2011-01-01T00:00:00";
2002       int julianStart = getJulianDay(startTime, 0);
2003       int julianEnd = getJulianDay(endTime, 0);
2004       Uri uri = Uri.withAppendedPath(
2005           CalendarContract.Instances.CONTENT_BY_DAY_URI, julianStart + "/" + julianEnd);
2006 
2007       // Query the range, sorting by event start time
2008       Cursor c = mContentResolver.query(uri, null, Instances.CALENDAR_ID + "="
2009               + helper.getCalendarId(), null, Events.DTSTART);
2010 
2011       // Assert that two events are returned
2012       assertEquals(c.getCount(), 2);
2013 
2014       Set<String> keySet = new HashSet();
2015       keySet.add(Events.DESCRIPTION);
2016       keySet.add(Events.DTSTART);
2017       keySet.add(Events.DTEND);
2018       keySet.add(Events.EVENT_TIMEZONE);
2019 
2020       // Verify that the contents of those two events match the cursor results
2021       verifyContentValuesAgainstCursor(eventsWithinRange, keySet, c);
2022     }
2023 
verifyContentValuesAgainstCursor(List<ContentValues> cvs, Set<String> keys, Cursor cursor)2024     private void verifyContentValuesAgainstCursor(List<ContentValues> cvs,
2025         Set<String> keys, Cursor cursor) {
2026       assertEquals(cursor.getCount(), cvs.size());
2027 
2028       cursor.moveToFirst();
2029 
2030       int i=0;
2031       do {
2032         ContentValues cv = cvs.get(i);
2033         for (String key : keys) {
2034           assertEquals(cv.get(key).toString(),
2035                   cursor.getString(cursor.getColumnIndex(key)));
2036         }
2037         i++;
2038       } while (cursor.moveToNext());
2039 
2040       cursor.close();
2041     }
2042 
timeInMillis(String timeString, int timeZoneIndex)2043     private long timeInMillis(String timeString, int timeZoneIndex) {
2044       Time startTime = new Time(TIME_ZONES[timeZoneIndex]);
2045       startTime.parse3339(timeString);
2046       return startTime.toMillis(false);
2047     }
2048 
getJulianDay(String timeString, int timeZoneIndex)2049     private int getJulianDay(String timeString, int timeZoneIndex) {
2050       Time time = new Time(TIME_ZONES[timeZoneIndex]);
2051       time.parse3339(timeString);
2052       return Time.getJulianDay(time.toMillis(false), time.gmtoff);
2053     }
2054 
2055     /**
2056      * Test instance queries with search parameters.
2057      */
2058     @MediumTest
testInstanceSearch()2059     public void testInstanceSearch() {
2060         String account = "cser_account";
2061         int seed = 0;
2062 
2063         // Clean up just in case
2064         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
2065 
2066         // Create a calendar
2067         ContentValues values = CalendarHelper.getNewCalendarValues(account, seed);
2068         long calendarId = createAndVerifyCalendar(account, seed++, values);
2069 
2070         String testStart = "2009-10-01T08:00:00";
2071         String timeZone = TIME_ZONES[0];
2072         Time startTime = new Time(timeZone);
2073         startTime.parse3339(testStart);
2074         long startMillis = startTime.toMillis(false);
2075 
2076         // Create some events, with different descriptions.  (Could also create a single
2077         // recurring event and some instance exceptions.)
2078         ContentValues eventValues;
2079         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
2080         eventValues.put(Events.DESCRIPTION, "testevent event-one fiddle");
2081         eventValues.put(Events.DTSTART, startMillis);
2082         eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS);
2083         eventValues.put(Events.EVENT_TIMEZONE, timeZone);
2084         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
2085         assertTrue(eventId1 >= 0);
2086 
2087         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
2088         eventValues.put(Events.DESCRIPTION, "testevent event-two fuzzle");
2089         eventValues.put(Events.DTSTART, startMillis + DateUtils.HOUR_IN_MILLIS);
2090         eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 2);
2091         eventValues.put(Events.EVENT_TIMEZONE, timeZone);
2092         long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
2093         assertTrue(eventId2 >= 0);
2094 
2095         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
2096         eventValues.put(Events.DESCRIPTION, "testevent event-three fiddle");
2097         eventValues.put(Events.DTSTART, startMillis + DateUtils.HOUR_IN_MILLIS * 2);
2098         eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 3);
2099         eventValues.put(Events.EVENT_TIMEZONE, timeZone);
2100         long eventId3 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
2101         assertTrue(eventId3 >= 0);
2102 
2103         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
2104         eventValues.put(Events.DESCRIPTION, "nontestevent");
2105         eventValues.put(Events.DTSTART, startMillis + (long) (DateUtils.HOUR_IN_MILLIS * 1.5f));
2106         eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 2);
2107         eventValues.put(Events.EVENT_TIMEZONE, timeZone);
2108         long eventId4 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
2109         assertTrue(eventId4 >= 0);
2110 
2111         String rangeStart = "2009-10-01T00:00:00";
2112         String rangeEnd = "2009-10-01T11:59:59";
2113         String[] projection = new String[] { Instances.BEGIN };
2114 
2115         if (false) {
2116             Cursor instances = getInstances(timeZone, rangeStart, rangeEnd, projection,
2117                     new long[] { calendarId });
2118             dumpInstances(instances, timeZone, "all");
2119             instances.close();
2120         }
2121 
2122         Cursor instances;
2123         int count;
2124 
2125         // Find all matching "testevent".  The search matches on partial strings, so this
2126         // will also pick up "nontestevent".
2127         instances = getInstancesSearch(timeZone, rangeStart, rangeEnd,
2128                 "testevent", false, projection, new long[] { calendarId });
2129         count = instances.getCount();
2130         instances.close();
2131         assertEquals(4, count);
2132 
2133         // Find all matching "fiddle" and "event".  Set the "by day" flag just to be different.
2134         instances = getInstancesSearch(timeZone, rangeStart, rangeEnd,
2135                 "fiddle event", true, projection, new long[] { calendarId });
2136         count = instances.getCount();
2137         instances.close();
2138         assertEquals(2, count);
2139 
2140         // Find all matching "fiddle" and "baluchitherium".
2141         instances = getInstancesSearch(timeZone, rangeStart, rangeEnd,
2142                 "baluchitherium fiddle", false, projection, new long[] { calendarId });
2143         count = instances.getCount();
2144         instances.close();
2145         assertEquals(0, count);
2146 
2147         // Find all matching "event-two".
2148         instances = getInstancesSearch(timeZone, rangeStart, rangeEnd,
2149                 "event-two", false, projection, new long[] { calendarId });
2150         count = instances.getCount();
2151         instances.close();
2152         assertEquals(1, count);
2153 
2154         removeAndVerifyCalendar(account, calendarId);
2155     }
2156 
2157     @MediumTest
testCalendarUpdateAsApp()2158     public void testCalendarUpdateAsApp() {
2159         String account = "cu1_account";
2160         int seed = 0;
2161 
2162         // Clean up just in case
2163         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
2164 
2165         // Create a calendar
2166         ContentValues values = CalendarHelper.getNewCalendarValues(account, seed);
2167         long id = createAndVerifyCalendar(account, seed++, values);
2168 
2169         Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
2170 
2171         // Update the calendar using the direct Uri
2172         ContentValues updateValues = CalendarHelper.getUpdateCalendarValuesWithOriginal(
2173                 values, seed++);
2174         assertEquals(1, mContentResolver.update(uri, updateValues, null, null));
2175 
2176         verifyCalendar(account, values, id, 1);
2177 
2178         // Update the calendar using selection + args
2179         String selection = Calendars._ID + "=?";
2180         String[] selectionArgs = new String[] { Long.toString(id) };
2181 
2182         updateValues = CalendarHelper.getUpdateCalendarValuesWithOriginal(values, seed++);
2183 
2184         assertEquals(1, mContentResolver.update(
2185                 Calendars.CONTENT_URI, updateValues, selection, selectionArgs));
2186 
2187         verifyCalendar(account, values, id, 1);
2188 
2189         removeAndVerifyCalendar(account, id);
2190     }
2191 
2192     // TODO test calendar updates as sync adapter
2193 
2194     /**
2195      * Test access to the "syncstate" table.
2196      */
2197     @MediumTest
testSyncState()2198     public void testSyncState() {
2199         String account = "ss_account";
2200         int seed = 0;
2201 
2202         // Clean up just in case
2203         SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, true);
2204 
2205         // Create a new sync state entry
2206         ContentValues values = SyncStateHelper.getNewSyncStateValues(account);
2207         long id = createAndVerifySyncState(account, values);
2208 
2209         // Look it up with the by-ID URI
2210         Cursor c = SyncStateHelper.getSyncStateById(mContentResolver, id);
2211         assertNotNull(c);
2212         assertEquals(1, c.getCount());
2213         c.close();
2214 
2215         // Try to remove it as non-sync-adapter; expected to fail.
2216         boolean failed;
2217         try {
2218             SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, false);
2219             failed = false;
2220         } catch (IllegalArgumentException iae) {
2221             failed = true;
2222         }
2223         assertTrue("deletion of sync state by app was allowed", failed);
2224 
2225         // Remove it and verify that it's gone
2226         removeAndVerifySyncState(account);
2227     }
2228 
2229 
verifyEvent(ContentValues values, long eventId)2230     private void verifyEvent(ContentValues values, long eventId) {
2231         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
2232         // Verify
2233         Cursor c = mContentResolver
2234                 .query(eventUri, EventHelper.EVENTS_PROJECTION, null, null, null);
2235         assertEquals(1, c.getCount());
2236         assertTrue(c.moveToFirst());
2237         assertEquals(eventId, c.getLong(0));
2238         for (String key : values.keySet()) {
2239             int index = c.getColumnIndex(key);
2240             assertEquals(key, values.getAsString(key), c.getString(index));
2241         }
2242         c.close();
2243     }
2244 
2245     @MediumTest
testEventCreationAndDeletion()2246     public void testEventCreationAndDeletion() {
2247         String account = "ec1_account";
2248         int seed = 0;
2249 
2250         // Clean up just in case
2251         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
2252 
2253         // Create calendar and event
2254         long calendarId = createAndVerifyCalendar(account, seed++, null);
2255 
2256         ContentValues eventValues = EventHelper
2257                 .getNewEventValues(account, seed++, calendarId, true);
2258         long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
2259 
2260         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
2261 
2262         removeAndVerifyEvent(eventUri, eventValues, account);
2263 
2264         // Attempt to create an event without a calendar ID.
2265         ContentValues badValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
2266         badValues.remove(Events.CALENDAR_ID);
2267         try {
2268             createAndVerifyEvent(account, seed, calendarId, true, badValues);
2269             fail("was allowed to create an event without CALENDAR_ID");
2270         } catch (IllegalArgumentException iae) {
2271             // expected
2272         }
2273 
2274         // Validation may be relaxed for content providers, so test missing timezone as app.
2275         badValues = EventHelper.getNewEventValues(account, seed++, calendarId, false);
2276         badValues.remove(Events.EVENT_TIMEZONE);
2277         try {
2278             createAndVerifyEvent(account, seed, calendarId, false, badValues);
2279             fail("was allowed to create an event without EVENT_TIMEZONE");
2280         } catch (IllegalArgumentException iae) {
2281             // expected
2282         }
2283 
2284         removeAndVerifyCalendar(account, calendarId);
2285     }
2286 
2287     @MediumTest
testEventUpdateAsApp()2288     public void testEventUpdateAsApp() {
2289         String account = "em1_account";
2290         int seed = 0;
2291 
2292         // Clean up just in case
2293         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
2294 
2295         // Create calendar
2296         long calendarId = createAndVerifyCalendar(account, seed++, null);
2297 
2298         // Create event as sync adapter
2299         ContentValues eventValues = EventHelper
2300                 .getNewEventValues(account, seed++, calendarId, true);
2301         long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
2302 
2303         // Update event as app
2304         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
2305 
2306         ContentValues updateValues = EventHelper.getUpdateEventValuesWithOriginal(eventValues,
2307                 seed++, false);
2308         assertEquals(1, mContentResolver.update(eventUri, updateValues, null, null));
2309         updateValues.put(Events.DIRTY, 1);      // provider should have marked as dirty
2310         verifyEvent(updateValues, eventId);
2311 
2312         // Try nulling out a required value.
2313         ContentValues badValues = new ContentValues(updateValues);
2314         badValues.putNull(Events.EVENT_TIMEZONE);
2315         badValues.remove(Events.DIRTY);
2316         try {
2317             mContentResolver.update(eventUri, badValues, null, null);
2318             fail("was allowed to null out EVENT_TIMEZONE");
2319         } catch (IllegalArgumentException iae) {
2320             // good
2321         }
2322 
2323         removeAndVerifyEvent(eventUri, eventValues, account);
2324 
2325         // delete the calendar
2326         removeAndVerifyCalendar(account, calendarId);
2327     }
2328 
2329     /**
2330      * Tests update of multiple events with a single update call.
2331      */
2332     @MediumTest
testBulkUpdate()2333     public void testBulkUpdate() {
2334         String account = "bup_account";
2335         int seed = 0;
2336 
2337         // Clean up just in case
2338         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
2339 
2340         // Create calendar
2341         long calendarId = createAndVerifyCalendar(account, seed++, null);
2342         String calendarIdStr = String.valueOf(calendarId);
2343 
2344         // Create events
2345         ContentValues eventValues;
2346         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
2347         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
2348 
2349         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
2350         long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
2351 
2352         // Update the "description" field in all events in this calendar.
2353         String newDescription = "bulk edit";
2354         ContentValues updateValues = new ContentValues();
2355         updateValues.put(Events.DESCRIPTION, newDescription);
2356 
2357         // Must be sync adapter to do a bulk update.
2358         Uri uri = asSyncAdapter(Events.CONTENT_URI, account, CTS_TEST_TYPE);
2359         int count = mContentResolver.update(uri, updateValues, SQL_WHERE_CALENDAR_ID,
2360                 new String[] { calendarIdStr });
2361 
2362         // Check to see if the changes went through.
2363         Uri eventUri = Events.CONTENT_URI;
2364         Cursor c = mContentResolver.query(eventUri, new String[] { Events.DESCRIPTION },
2365                 SQL_WHERE_CALENDAR_ID, new String[] { calendarIdStr }, null);
2366         assertEquals(2, c.getCount());
2367         while (c.moveToNext()) {
2368             assertEquals(newDescription, c.getString(0));
2369         }
2370         c.close();
2371 
2372         // delete the calendar
2373         removeAndVerifyCalendar(account, calendarId);
2374     }
2375 
2376     /**
2377      * Tests the content provider's enforcement of restrictions on who is allowed to modify
2378      * specific columns in a Calendar.
2379      * <p>
2380      * This attempts to create a new row in the Calendar table, specifying one restricted
2381      * column at a time.
2382      */
2383     @MediumTest
testSyncOnlyInsertEnforcement()2384     public void testSyncOnlyInsertEnforcement() {
2385         // These operations should not succeed, so there should be nothing to clean up after.
2386         // TODO: this should be a new event augmented with an illegal column, not a single
2387         //       column.  Otherwise we might be tripping over a "DTSTART must exist" test.
2388         ContentValues vals = new ContentValues();
2389         for (int i = 0; i < Calendars.SYNC_WRITABLE_COLUMNS.length; i++) {
2390             boolean threw = false;
2391             try {
2392                 vals.clear();
2393                 vals.put(Calendars.SYNC_WRITABLE_COLUMNS[i], "1");
2394                 mContentResolver.insert(Calendars.CONTENT_URI, vals);
2395             } catch (IllegalArgumentException e) {
2396                 threw = true;
2397             }
2398             assertTrue("Only sync adapter should be allowed to insert "
2399                     + Calendars.SYNC_WRITABLE_COLUMNS[i], threw);
2400         }
2401     }
2402 
2403     /**
2404      * Tests creation of a recurring event.
2405      * <p>
2406      * This (and the other recurrence tests) uses dates well in the past to reduce the likelihood
2407      * of encountering non-test recurring events.  (Ideally we would select events associated
2408      * with a specific calendar.)  With dates well in the past, it's also important to have a
2409      * fixed maximum count or end date; otherwise, if the metadata min/max instance values are
2410      * large enough, the recurrence recalculation processor could get triggered on an insert or
2411      * update and bump up against the 2000-instance limit.
2412      *
2413      * TODO: need some allDay tests
2414      */
2415     @MediumTest
testRecurrence()2416     public void testRecurrence() {
2417         String account = "re_account";
2418         int seed = 0;
2419 
2420         // Clean up just in case
2421         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
2422 
2423         // Create calendar
2424         long calendarId = createAndVerifyCalendar(account, seed++, null);
2425 
2426         // Create recurring event
2427         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
2428                 calendarId, true, "2003-08-05T09:00:00", "PT1H",
2429                 "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU");
2430         long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
2431         //Log.d(TAG, "+++ basic recurrence eventId is " + eventId);
2432 
2433         // Check to see if we have the expected number of instances
2434         String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
2435         int instanceCount = getInstanceCount(timeZone, "2003-08-05T00:00:00",
2436                 "2003-08-31T11:59:59", new long[] { calendarId });
2437         if (false) {
2438             Cursor instances = getInstances(timeZone, "2003-08-05T00:00:00", "2003-08-31T11:59:59",
2439                     new String[] { Instances.BEGIN }, new long[] { calendarId });
2440             dumpInstances(instances, timeZone, "initial");
2441             instances.close();
2442         }
2443         assertEquals("recurrence instance count", 4, instanceCount);
2444 
2445         // delete the calendar
2446         removeAndVerifyCalendar(account, calendarId);
2447     }
2448 
2449     /**
2450      * Tests conversion of a regular event to a recurring event.
2451      */
2452     @MediumTest
testConversionToRecurring()2453     public void testConversionToRecurring() {
2454         String account = "reconv_account";
2455         int seed = 0;
2456 
2457         // Clean up just in case
2458         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
2459 
2460         // Create calendar and event
2461         long calendarId = createAndVerifyCalendar(account, seed++, null);
2462 
2463         ContentValues eventValues = EventHelper
2464                 .getNewEventValues(account, seed++, calendarId, true);
2465         long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
2466 
2467         long dtstart = eventValues.getAsLong(Events.DTSTART);
2468         long dtend = eventValues.getAsLong(Events.DTEND);
2469         long durationSecs = (dtend - dtstart) / 1000;
2470 
2471         ContentValues updateValues = new ContentValues();
2472         updateValues.put(Events.RRULE, "FREQ=WEEKLY");   // recurs forever
2473         updateValues.put(Events.DURATION, "P" + durationSecs + "S");
2474         updateValues.putNull(Events.DTEND);
2475 
2476         // Issue update; do it as app instead of sync adapter to exercise that path.
2477         updateAndVerifyEvent(account, calendarId, eventId, false, updateValues);
2478 
2479         // Make sure LAST_DATE got nulled out by our infinitely repeating sequence.
2480         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
2481         Cursor c = mContentResolver.query(eventUri, new String[] { Events.LAST_DATE },
2482                 null, null, null);
2483         assertEquals(1, c.getCount());
2484         assertTrue(c.moveToFirst());
2485         assertNull(c.getString(0));
2486         c.close();
2487 
2488         removeAndVerifyCalendar(account, calendarId);
2489     }
2490 
2491     /**
2492      * Tests creation of a recurring event with single-instance exceptions.
2493      */
2494     @MediumTest
testSingleRecurrenceExceptions()2495     public void testSingleRecurrenceExceptions() {
2496         String account = "rex_account";
2497         int seed = 0;
2498 
2499         // Clean up just in case
2500         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
2501 
2502         // Create calendar
2503         long calendarId = createAndVerifyCalendar(account, seed++, null);
2504 
2505         // Create recurring event.
2506         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
2507                 calendarId, true, "1999-03-28T09:00:00", "PT1H", "FREQ=WEEKLY;WKST=SU;COUNT=100");
2508         long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues);
2509 
2510         // Add some attendees and reminders.
2511         addAttendees(account, eventId, seed);
2512         addReminders(account, eventId, seed);
2513 
2514         // Select a period that gives us 5 instances.  We don't want this to straddle a DST
2515         // transition, because we expect the startMinute field to be the same for all
2516         // instances, and it's stored as minutes since midnight in the device's time zone.
2517         // Things won't be consistent if the event and the device have different ideas about DST.
2518         String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
2519         String testStart = "1999-07-02T00:00:00";
2520         String testEnd = "1999-08-04T23:59:59";
2521         String[] projection = { Instances.BEGIN, Instances.START_MINUTE, Instances.END_MINUTE };
2522 
2523         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
2524                 new long[] { calendarId });
2525         if (DEBUG_RECURRENCE) {
2526             dumpInstances(instances, timeZone, "initial");
2527         }
2528 
2529         assertEquals("initial recurrence instance count", 5, instances.getCount());
2530 
2531         /*
2532          * Advance the start time of a few instances, and verify.
2533          */
2534 
2535         // Leave first instance alone.
2536         instances.moveToPosition(1);
2537 
2538         long startMillis;
2539         ContentValues excepValues;
2540 
2541         // Advance the start time of the 2nd instance.
2542         startMillis = instances.getLong(0);
2543         excepValues = EventHelper.getNewExceptionValues(startMillis);
2544         excepValues.put(Events.DTSTART, startMillis + 3600*1000);
2545         long excepEventId2 = createAndVerifyException(account, eventId, excepValues, true);
2546         instances.moveToNext();
2547 
2548         // Advance the start time of the 3rd instance.
2549         startMillis = instances.getLong(0);
2550         excepValues = EventHelper.getNewExceptionValues(startMillis);
2551         excepValues.put(Events.DTSTART, startMillis + 3600*1000*2);
2552         long excepEventId3 = createAndVerifyException(account, eventId, excepValues, true);
2553         instances.moveToNext();
2554 
2555         // Cancel the 4th instance.
2556         startMillis = instances.getLong(0);
2557         excepValues = EventHelper.getNewExceptionValues(startMillis);
2558         excepValues.put(Events.STATUS, Events.STATUS_CANCELED);
2559         long excepEventId4 = createAndVerifyException(account, eventId, excepValues, true);
2560         instances.moveToNext();
2561 
2562         // TODO: try to modify a non-existent instance.
2563 
2564         instances.close();
2565 
2566         // TODO: compare Reminders, Attendees, ExtendedProperties on one of the exception events
2567 
2568         // Re-query the instances and figure out if they look right.
2569         instances = getInstances(timeZone, testStart, testEnd, projection,
2570                 new long[] { calendarId });
2571         if (DEBUG_RECURRENCE) {
2572             dumpInstances(instances, timeZone, "with DTSTART exceptions");
2573         }
2574         assertEquals("exceptional recurrence instance count", 4, instances.getCount());
2575 
2576         long prevMinute = -1;
2577         while (instances.moveToNext()) {
2578             // expect the start times for each entry to be different from the previous entry
2579             long startMinute = instances.getLong(1);
2580             assertTrue("instance start times are different", startMinute != prevMinute);
2581 
2582             prevMinute = startMinute;
2583         }
2584         instances.close();
2585 
2586 
2587         // Delete all of our exceptions, and verify.
2588         int deleteCount = 0;
2589         deleteCount += deleteException(account, eventId, excepEventId2);
2590         deleteCount += deleteException(account, eventId, excepEventId3);
2591         deleteCount += deleteException(account, eventId, excepEventId4);
2592         assertEquals("events deleted", 3, deleteCount);
2593 
2594         // Re-query the instances and figure out if they look right.
2595         instances = getInstances(timeZone, testStart, testEnd, projection,
2596                 new long[] { calendarId });
2597         if (DEBUG_RECURRENCE) {
2598             dumpInstances(instances, timeZone, "post exception deletion");
2599         }
2600         assertEquals("post-exception deletion instance count", 5, instances.getCount());
2601 
2602         prevMinute = -1;
2603         while (instances.moveToNext()) {
2604             // expect the start times for each entry to be the same
2605             long startMinute = instances.getLong(1);
2606             if (prevMinute != -1) {
2607                 assertEquals("instance start times are the same", startMinute, prevMinute);
2608             }
2609             prevMinute = startMinute;
2610         }
2611         instances.close();
2612 
2613         /*
2614          * Repeat the test, this time modifying DURATION.
2615          */
2616 
2617         instances = getInstances(timeZone, testStart, testEnd, projection,
2618                 new long[] { calendarId });
2619         if (DEBUG_RECURRENCE) {
2620             dumpInstances(instances, timeZone, "initial");
2621         }
2622 
2623         assertEquals("initial recurrence instance count", 5, instances.getCount());
2624 
2625         // Leave first instance alone.
2626         instances.moveToPosition(1);
2627 
2628         // Advance the end time of the 2nd instance.
2629         startMillis = instances.getLong(0);
2630         excepValues = EventHelper.getNewExceptionValues(startMillis);
2631         excepValues.put(Events.DURATION, "P" + 3600*2 + "S");
2632         excepEventId2 = createAndVerifyException(account, eventId, excepValues, true);
2633         instances.moveToNext();
2634 
2635         // Advance the end time of the 3rd instance, and change the self-attendee status.
2636         startMillis = instances.getLong(0);
2637         excepValues = EventHelper.getNewExceptionValues(startMillis);
2638         excepValues.put(Events.DURATION, "P" + 3600*3 + "S");
2639         excepValues.put(Events.SELF_ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED);
2640         excepEventId3 = createAndVerifyException(account, eventId, excepValues, true);
2641         instances.moveToNext();
2642 
2643         // Advance the start time of the 4th instance, which will also advance the end time.
2644         startMillis = instances.getLong(0);
2645         excepValues = EventHelper.getNewExceptionValues(startMillis);
2646         excepValues.put(Events.DTSTART, startMillis + 3600*1000);
2647         excepEventId4 = createAndVerifyException(account, eventId, excepValues, true);
2648         instances.moveToNext();
2649 
2650         instances.close();
2651 
2652         // TODO: make sure the selfAttendeeStatus change took
2653 
2654         // Re-query the instances and figure out if they look right.
2655         instances = getInstances(timeZone, testStart, testEnd, projection,
2656                 new long[] { calendarId });
2657         if (DEBUG_RECURRENCE) {
2658             dumpInstances(instances, timeZone, "with DURATION exceptions");
2659         }
2660         assertEquals("exceptional recurrence instance count", 5, instances.getCount());
2661 
2662         prevMinute = -1;
2663         while (instances.moveToNext()) {
2664             // expect the start times for each entry to be different from the previous entry
2665             long endMinute = instances.getLong(2);
2666             assertTrue("instance end times are different", endMinute != prevMinute);
2667 
2668             prevMinute = endMinute;
2669         }
2670         instances.close();
2671 
2672         // delete the calendar
2673         removeAndVerifyCalendar(account, calendarId);
2674     }
2675 
2676     /**
2677      * Tests creation of a simple recurrence exception when not pretending to be the sync
2678      * adapter.  One significant consequence is that we don't set the _sync_id field in the
2679      * events, which affects how the provider correlates recurrences and exceptions.
2680      */
2681     @MediumTest
testNonAdapterRecurrenceExceptions()2682     public void testNonAdapterRecurrenceExceptions() {
2683         String account = "rena_account";
2684         int seed = 0;
2685 
2686         // Clean up just in case
2687         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
2688 
2689         // Create calendar
2690         long calendarId = createAndVerifyCalendar(account, seed++, null);
2691 
2692         // Generate recurring event, with "asSyncAdapter" set to false.
2693         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
2694                 calendarId, false, "1991-02-03T12:00:00", "PT1H", "FREQ=DAILY;WKST=SU;COUNT=10");
2695 
2696         // Select a period that gives us 3 instances.
2697         String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
2698         String testStart = "1991-02-03T00:00:00";
2699         String testEnd = "1991-02-05T23:59:59";
2700         String[] projection = { Instances.BEGIN, Instances.START_MINUTE };
2701 
2702         // Expand the bounds of the instances table so we expand future events as they are added.
2703         expandInstanceRange(account, calendarId, testStart, testEnd, timeZone);
2704 
2705         // Create the event in the database.
2706         long eventId = createAndVerifyEvent(account, seed++, calendarId, false, eventValues);
2707         assertTrue(eventId >= 0);
2708 
2709         // Add some attendees.
2710         addAttendees(account, eventId, seed);
2711 
2712         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
2713                 new long[] { calendarId });
2714         if (DEBUG_RECURRENCE) {
2715             dumpInstances(instances, timeZone, "initial");
2716         }
2717         assertEquals("initial recurrence instance count", 3, instances.getCount());
2718 
2719         /*
2720          * Alter the attendee status of the second event.  This should cause the instances to
2721          * be updated, replacing the previous 2nd instance with the exception instance.  If the
2722          * code is broken we'll see four instances (because the original instance didn't get
2723          * removed) or one instance (because the code correctly deleted all related events but
2724          * couldn't correlate the exception with its original recurrence).
2725          */
2726 
2727         // Leave first instance alone.
2728         instances.moveToPosition(1);
2729 
2730         long startMillis;
2731         ContentValues excepValues;
2732 
2733         // Advance the start time of the 2nd instance.
2734         startMillis = instances.getLong(0);
2735         excepValues = EventHelper.getNewExceptionValues(startMillis);
2736         excepValues.put(Events.SELF_ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED);
2737         long excepEventId2 = createAndVerifyException(account, eventId, excepValues, false);
2738         instances.moveToNext();
2739 
2740         instances.close();
2741 
2742         // Re-query the instances and figure out if they look right.
2743         instances = getInstances(timeZone, testStart, testEnd, projection,
2744                 new long[] { calendarId });
2745         if (DEBUG_RECURRENCE) {
2746             dumpInstances(instances, timeZone, "with exceptions");
2747         }
2748 
2749         // TODO: this test currently fails due to limitations in the provider
2750         //assertEquals("exceptional recurrence instance count", 3, instances.getCount());
2751 
2752         instances.close();
2753 
2754         // delete the calendar
2755         removeAndVerifyCalendar(account, calendarId);
2756     }
2757 
2758     /**
2759      * Tests insertion of event exceptions before and after a recurring event is created.
2760      * <p>
2761      * The server may send exceptions down before the event they refer to, so the provider
2762      * fills in the originalId of previously-existing exceptions when a recurring event is
2763      * inserted.  Make sure that works.
2764      * <p>
2765      * The _sync_id column is only unique with a given calendar.  We create events with
2766      * identical originalSyncId values in two different calendars to verify that the provider
2767      * doesn't update unrelated events.
2768      * <p>
2769      * We can't use the /exception URI, because that only works if the events are created
2770      * in order.
2771      */
2772     @MediumTest
testOutOfOrderRecurrenceExceptions()2773     public void testOutOfOrderRecurrenceExceptions() {
2774         String account1 = "roid1_account";
2775         String account2 = "roid2_account";
2776         String startWhen = "1987-08-09T12:00:00";
2777         int seed = 0;
2778 
2779         // Clean up just in case
2780         CalendarHelper.deleteCalendarByAccount(mContentResolver, account1);
2781         CalendarHelper.deleteCalendarByAccount(mContentResolver, account2);
2782 
2783         // Create calendars
2784         long calendarId1 = createAndVerifyCalendar(account1, seed++, null);
2785         long calendarId2 = createAndVerifyCalendar(account2, seed++, null);
2786 
2787 
2788         // Generate base event.
2789         ContentValues recurEventValues = EventHelper.getNewRecurringEventValues(account1, seed++,
2790                 calendarId1, true, startWhen, "PT1H", "FREQ=DAILY;WKST=SU;COUNT=10");
2791 
2792         // Select a period that gives us 3 instances.
2793         String timeZone = recurEventValues.getAsString(Events.EVENT_TIMEZONE);
2794         String testStart = "1987-08-09T00:00:00";
2795         String testEnd = "1987-08-11T23:59:59";
2796         String[] projection = { Instances.BEGIN, Instances.START_MINUTE, Instances.EVENT_ID };
2797 
2798         /*
2799          * We're interested in exploring what the instance expansion code does with the events
2800          * as they arrive.  It won't do anything at event-creation time unless the instance
2801          * range already covers the interesting set of dates, so we need to create and remove
2802          * an instance in the same time frame beforehand.
2803          */
2804         expandInstanceRange(account1, calendarId1, testStart, testEnd, timeZone);
2805 
2806         /*
2807          * Instances table should be expanded.  Do the test.
2808          */
2809 
2810         final String MAGIC_SYNC_ID = "MagicSyncId";
2811         recurEventValues.put(Events._SYNC_ID, MAGIC_SYNC_ID);
2812 
2813         // Generate exceptions from base, removing the generated _sync_id and setting the
2814         // base event's _sync_id as originalSyncId.
2815         ContentValues beforeExcepValues, afterExcepValues, unrelatedExcepValues;
2816         beforeExcepValues = new ContentValues(recurEventValues);
2817         afterExcepValues = new ContentValues(recurEventValues);
2818         unrelatedExcepValues = new ContentValues(recurEventValues);
2819         beforeExcepValues.remove(Events._SYNC_ID);
2820         afterExcepValues.remove(Events._SYNC_ID);
2821         unrelatedExcepValues.remove(Events._SYNC_ID);
2822         beforeExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
2823         afterExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
2824         unrelatedExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
2825 
2826         // Disassociate the "unrelated" exception by moving it to the other calendar.
2827         unrelatedExcepValues.put(Events.CALENDAR_ID, calendarId2);
2828 
2829         // We shift the start time by half an hour, and use the same _sync_id.
2830         final long ONE_DAY_MILLIS = 24 * 60 * 60 * 1000;
2831         final long ONE_HOUR_MILLIS  = 60 * 60 * 1000;
2832         final long HALF_HOUR_MILLIS  = 30 * 60 * 1000;
2833         long dtstartMillis = recurEventValues.getAsLong(Events.DTSTART) + ONE_DAY_MILLIS;
2834         beforeExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis);
2835         beforeExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS);
2836         beforeExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS);
2837         beforeExcepValues.remove(Events.DURATION);
2838         beforeExcepValues.remove(Events.RRULE);
2839         beforeExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
2840         dtstartMillis += ONE_DAY_MILLIS;
2841         afterExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis);
2842         afterExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS);
2843         afterExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS);
2844         afterExcepValues.remove(Events.DURATION);
2845         afterExcepValues.remove(Events.RRULE);
2846         afterExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
2847         dtstartMillis += ONE_DAY_MILLIS;
2848         unrelatedExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis);
2849         unrelatedExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS);
2850         unrelatedExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS);
2851         unrelatedExcepValues.remove(Events.DURATION);
2852         unrelatedExcepValues.remove(Events.RRULE);
2853         unrelatedExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
2854 
2855 
2856         // Create "before" and "unrelated" exceptions.
2857         long beforeEventId = createAndVerifyEvent(account1, seed, calendarId1, true,
2858                 beforeExcepValues);
2859         assertTrue(beforeEventId >= 0);
2860         long unrelatedEventId = createAndVerifyEvent(account2, seed, calendarId2, true,
2861                 unrelatedExcepValues);
2862         assertTrue(unrelatedEventId >= 0);
2863 
2864         // Create recurring event.
2865         long recurEventId = createAndVerifyEvent(account1, seed, calendarId1, true,
2866                 recurEventValues);
2867         assertTrue(recurEventId >= 0);
2868 
2869         // Create "after" exception.
2870         long afterEventId = createAndVerifyEvent(account1, seed, calendarId1, true,
2871                 afterExcepValues);
2872         assertTrue(afterEventId >= 0);
2873 
2874         if (Log.isLoggable(TAG, Log.DEBUG)) {
2875             Log.d(TAG, "before=" + beforeEventId + ", unrel=" + unrelatedEventId +
2876                     ", recur=" + recurEventId + ", after=" + afterEventId);
2877         }
2878 
2879         // Check to see how many instances we get.  If the recurrence and the exception don't
2880         // get paired up correctly, we'll see too many instances.
2881         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
2882                 new long[] { calendarId1, calendarId2 });
2883         if (DEBUG_RECURRENCE) {
2884             dumpInstances(instances, timeZone, "with exception");
2885         }
2886 
2887         assertEquals("initial recurrence instance count", 3, instances.getCount());
2888 
2889         instances.close();
2890 
2891 
2892         /*
2893          * Now we want to verify that:
2894          * - "before" and "after" have an originalId equal to our recurEventId
2895          * - "unrelated" has no originalId
2896          */
2897         Cursor c = null;
2898         try {
2899             final String[] PROJECTION = new String[] { Events.ORIGINAL_ID };
2900             Uri eventUri;
2901             Long originalId;
2902 
2903             eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, beforeEventId);
2904             c = mContentResolver.query(eventUri, PROJECTION, null, null, null);
2905             assertEquals(1, c.getCount());
2906             c.moveToNext();
2907             originalId = c.getLong(0);
2908             assertNotNull(originalId);
2909             assertEquals(recurEventId, (long) originalId);
2910             c.close();
2911 
2912             eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, afterEventId);
2913             c = mContentResolver.query(eventUri, PROJECTION, null, null, null);
2914             assertEquals(1, c.getCount());
2915             c.moveToNext();
2916             originalId = c.getLong(0);
2917             assertNotNull(originalId);
2918             assertEquals(recurEventId, (long) originalId);
2919             c.close();
2920 
2921             eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, unrelatedEventId);
2922             c = mContentResolver.query(eventUri, PROJECTION, null, null, null);
2923             assertEquals(1, c.getCount());
2924             c.moveToNext();
2925             assertNull(c.getString(0));
2926             c.close();
2927 
2928             c = null;
2929         } finally {
2930             if (c != null) {
2931                 c.close();
2932             }
2933         }
2934 
2935         // delete the calendars
2936         removeAndVerifyCalendar(account1, calendarId1);
2937         removeAndVerifyCalendar(account2, calendarId2);
2938     }
2939 
2940     /**
2941      * Tests exceptions that modify all future instances of a recurring event.
2942      */
2943     @MediumTest
testForwardRecurrenceExceptions()2944     public void testForwardRecurrenceExceptions() {
2945         String account = "refx_account";
2946         int seed = 0;
2947 
2948         // Clean up just in case
2949         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
2950 
2951         // Create calendar
2952         long calendarId = createAndVerifyCalendar(account, seed++, null);
2953 
2954         // Create recurring event
2955         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
2956                 calendarId, true, "1999-01-01T06:00:00", "PT1H", "FREQ=WEEKLY;WKST=SU;COUNT=10");
2957         long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues);
2958 
2959         // Add some attendees and reminders.
2960         addAttendees(account, eventId, seed++);
2961         addReminders(account, eventId, seed++);
2962 
2963         // Get some instances.
2964         String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
2965         String testStart = "1999-01-01T00:00:00";
2966         String testEnd = "1999-01-29T23:59:59";
2967         String[] projection = { Instances.BEGIN, Instances.START_MINUTE };
2968 
2969         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
2970                 new long[] { calendarId });
2971         if (DEBUG_RECURRENCE) {
2972             dumpInstances(instances, timeZone, "initial");
2973         }
2974 
2975         assertEquals("initial recurrence instance count", 5, instances.getCount());
2976 
2977         // Modify starting from 3rd instance.
2978         instances.moveToPosition(2);
2979 
2980         long startMillis;
2981         ContentValues excepValues;
2982 
2983         // Replace with a new recurrence rule.  We move the start time an hour later, and cap
2984         // it at two instances.
2985         startMillis = instances.getLong(0);
2986         excepValues = EventHelper.getNewExceptionValues(startMillis);
2987         excepValues.put(Events.DTSTART, startMillis + 3600*1000);
2988         excepValues.put(Events.RRULE, "FREQ=WEEKLY;COUNT=2;WKST=SU");
2989         long excepEventId = createAndVerifyException(account, eventId, excepValues, true);
2990         instances.close();
2991 
2992 
2993         // Check to see if it took.
2994         instances = getInstances(timeZone, testStart, testEnd, projection,
2995                 new long[] { calendarId });
2996         if (DEBUG_RECURRENCE) {
2997             dumpInstances(instances, timeZone, "with new rule");
2998         }
2999 
3000         assertEquals("count with exception", 4, instances.getCount());
3001 
3002         long prevMinute = -1;
3003         for (int i = 0; i < 4; i++) {
3004             long startMinute;
3005             instances.moveToNext();
3006             switch (i) {
3007                 case 0:
3008                     startMinute = instances.getLong(1);
3009                     break;
3010                 case 1:
3011                 case 3:
3012                     startMinute = instances.getLong(1);
3013                     assertEquals("first/last pairs match", prevMinute, startMinute);
3014                     break;
3015                 case 2:
3016                     startMinute = instances.getLong(1);
3017                     assertFalse("first two != last two", prevMinute == startMinute);
3018                     break;
3019                 default:
3020                     fail();
3021                     startMinute = -1;   // make compiler happy
3022                     break;
3023             }
3024 
3025             prevMinute = startMinute;
3026         }
3027         instances.close();
3028 
3029         // delete the calendar
3030         removeAndVerifyCalendar(account, calendarId);
3031     }
3032 
3033     /**
3034      * Tests exceptions that modify all instances of a recurring event.  This is not really an
3035      * exception, since it won't create a new event, but supporting it allows us to use the
3036      * exception URI without having to determine whether the "start from here" instance is the
3037      * very first instance.
3038      */
3039     @MediumTest
testFullRecurrenceUpdate()3040     public void testFullRecurrenceUpdate() {
3041         String account = "ref_account";
3042         int seed = 0;
3043 
3044         // Clean up just in case
3045         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
3046 
3047         // Create calendar
3048         long calendarId = createAndVerifyCalendar(account, seed++, null);
3049 
3050         // Create recurring event
3051         String rrule = "FREQ=DAILY;WKST=MO;COUNT=100";
3052         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
3053                 calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule);
3054         long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues);
3055         //Log.i(TAG, "+++ eventId is " + eventId);
3056 
3057         // Get some instances.
3058         String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
3059         String testStart = "1997-08-01T00:00:00";
3060         String testEnd = "1997-08-31T23:59:59";
3061         String[] projection = { Instances.BEGIN, Instances.EVENT_LOCATION };
3062         String newLocation = "NEW!";
3063 
3064         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
3065                 new long[] { calendarId });
3066         if (DEBUG_RECURRENCE) {
3067             dumpInstances(instances, timeZone, "initial");
3068         }
3069 
3070         assertEquals("initial recurrence instance count", 3, instances.getCount());
3071 
3072         instances.moveToFirst();
3073         long startMillis = instances.getLong(0);
3074         ContentValues excepValues = EventHelper.getNewExceptionValues(startMillis);
3075         excepValues.put(Events.RRULE, rrule);   // identifies this as an "all future events" excep
3076         excepValues.put(Events.EVENT_LOCATION, newLocation);
3077         long excepEventId = createAndVerifyException(account, eventId, excepValues, true);
3078         instances.close();
3079 
3080         // Check results.
3081         assertEquals("full update does not create new ID", eventId, excepEventId);
3082 
3083         instances = getInstances(timeZone, testStart, testEnd, projection,
3084                 new long[] { calendarId });
3085         assertEquals("post-update instance count", 3, instances.getCount());
3086         while (instances.moveToNext()) {
3087             assertEquals("new location", newLocation, instances.getString(1));
3088         }
3089         instances.close();
3090 
3091         // delete the calendar
3092         removeAndVerifyCalendar(account, calendarId);
3093     }
3094 
3095     @MediumTest
testMultiRuleRecurrence()3096     public void testMultiRuleRecurrence() {
3097         String account = "multirule_account";
3098         int seed = 0;
3099 
3100         // Clean up just in case
3101         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
3102 
3103         // Create calendar
3104         long calendarId = createAndVerifyCalendar(account, seed++, null);
3105 
3106         // Create recurring event
3107         String rrule = "FREQ=DAILY;WKST=MO;COUNT=5\nFREQ=WEEKLY;WKST=SU;COUNT=5";
3108         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
3109                 calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule);
3110         long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues);
3111 
3112         // TODO: once multi-rule RRULEs are fully supported, verify that they work
3113 
3114         // delete the calendar
3115         removeAndVerifyCalendar(account, calendarId);
3116     }
3117 
3118     /**
3119      * Issue bad requests and expect them to get rejected.
3120      */
3121     @MediumTest
testBadRequests()3122     public void testBadRequests() {
3123         String account = "neg_account";
3124         int seed = 0;
3125 
3126         // Clean up just in case
3127         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
3128 
3129         // Create calendar
3130         long calendarId = createAndVerifyCalendar(account, seed++, null);
3131 
3132         // Create recurring event
3133         String rrule = "FREQ=OFTEN;WKST=MO";
3134         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
3135                 calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule);
3136         try {
3137             createAndVerifyEvent(account, seed++, calendarId, true, eventValues);
3138             fail("Bad recurrence rule should have been rejected");
3139         } catch (IllegalArgumentException iae) {
3140             // good
3141         }
3142 
3143         // delete the calendar
3144         removeAndVerifyCalendar(account, calendarId);
3145     }
3146 
3147     /**
3148      * Tests correct behavior of Calendars.isPrimary column
3149      */
3150     @MediumTest
testCalendarIsPrimary()3151     public void testCalendarIsPrimary() {
3152         String account = "ec_account";
3153         int seed = 0;
3154 
3155         // Clean up just in case
3156         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
3157 
3158         int isPrimary;
3159         Cursor cursor;
3160         ContentValues values = new ContentValues();
3161 
3162         final long calendarId = createAndVerifyCalendar(account, seed++, null);
3163         final Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calendarId);
3164 
3165         // verify when ownerAccount != account_name && isPrimary IS NULL
3166         cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null);
3167         cursor.moveToFirst();
3168         isPrimary = cursor.getInt(0);
3169         cursor.close();
3170         assertEquals("isPrimary should be 0 if ownerAccount != account_name", 0, isPrimary);
3171 
3172         // verify when ownerAccount == account_name && isPrimary IS NULL
3173         values.clear();
3174         values.put(Calendars.OWNER_ACCOUNT, account);
3175         mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null);
3176         cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null);
3177         cursor.moveToFirst();
3178         isPrimary = cursor.getInt(0);
3179         cursor.close();
3180         assertEquals("isPrimary should be 1 if ownerAccount == account_name", 1, isPrimary);
3181 
3182         // verify isPrimary IS NOT NULL
3183         values.clear();
3184         values.put(Calendars.IS_PRIMARY, SOME_ARBITRARY_INT);
3185         mContentResolver.update(uri, values, null, null);
3186         cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null);
3187         cursor.moveToFirst();
3188         isPrimary = cursor.getInt(0);
3189         cursor.close();
3190         assertEquals("isPrimary should be the value it was set to", SOME_ARBITRARY_INT, isPrimary);
3191 
3192         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
3193     }
3194 
3195     /**
3196      * Tests correct behavior of Events.isOrganizer column
3197      */
3198     @MediumTest
testEventsIsOrganizer()3199     public void testEventsIsOrganizer() {
3200         String account = "ec_account";
3201         int seed = 0;
3202 
3203         // Clean up just in case
3204         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
3205 
3206         int isOrganizer;
3207         Cursor cursor;
3208         ContentValues values = new ContentValues();
3209 
3210         final long calendarId = createAndVerifyCalendar(account, seed++, null);
3211         final long eventId = createAndVerifyEvent(account, seed, calendarId, true, null);
3212         final Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
3213 
3214         // verify when ownerAccount != organizer && isOrganizer IS NULL
3215         cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null);
3216         cursor.moveToFirst();
3217         isOrganizer = cursor.getInt(0);
3218         cursor.close();
3219         assertEquals("isOrganizer should be 0 if ownerAccount != organizer", 0, isOrganizer);
3220 
3221         // verify when ownerAccount == account_name && isOrganizer IS NULL
3222         values.clear();
3223         values.put(Events.ORGANIZER, CalendarHelper.generateCalendarOwnerEmail(account));
3224         mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null);
3225         cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null);
3226         cursor.moveToFirst();
3227         isOrganizer = cursor.getInt(0);
3228         cursor.close();
3229         assertEquals("isOrganizer should be 1 if ownerAccount == organizer", 1, isOrganizer);
3230 
3231         // verify isOrganizer IS NOT NULL
3232         values.clear();
3233         values.put(Events.IS_ORGANIZER, SOME_ARBITRARY_INT);
3234         mContentResolver.update(uri, values, null, null);
3235         cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null);
3236         cursor.moveToFirst();
3237         isOrganizer = cursor.getInt(0);
3238         cursor.close();
3239         assertEquals(
3240                 "isPrimary should be the value it was set to", SOME_ARBITRARY_INT, isOrganizer);
3241         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
3242     }
3243 
3244     /**
3245      * Tests correct behavior of Events.uid2445 column
3246      */
3247     @MediumTest
testEventsUid2445()3248     public void testEventsUid2445() {
3249         String account = "ec_account";
3250         int seed = 0;
3251 
3252         // Clean up just in case
3253         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
3254 
3255         final String uid = "uid_123";
3256         Cursor cursor;
3257         ContentValues values = new ContentValues();
3258         final long calendarId = createAndVerifyCalendar(account, seed++, null);
3259         final long eventId = createAndVerifyEvent(account, seed, calendarId, true, null);
3260         final Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
3261 
3262         // Verify default is null
3263         cursor = mContentResolver.query(uri, new String[] {Events.UID_2445}, null, null, null);
3264         cursor.moveToFirst();
3265         assertTrue(cursor.isNull(0));
3266         cursor.close();
3267 
3268         // Write column value and read back
3269         values.clear();
3270         values.put(Events.UID_2445, uid);
3271         mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null);
3272         cursor = mContentResolver.query(uri, new String[] {Events.UID_2445}, null, null, null);
3273         cursor.moveToFirst();
3274         assertFalse(cursor.isNull(0));
3275         assertEquals("Column uid_2445 has unexpected value.", uid, cursor.getString(0));
3276 
3277         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
3278     }
3279 
3280     @MediumTest
testMutatorSetCorrectly()3281     public void testMutatorSetCorrectly() {
3282         String account = "ec_account";
3283         String packageName = "android.provider.cts";
3284         int seed = 0;
3285 
3286         // Clean up just in case
3287         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
3288 
3289         String mutator;
3290         Cursor cursor;
3291         ContentValues values = new ContentValues();
3292         final long calendarId = createAndVerifyCalendar(account, seed++, null);
3293 
3294         // Verify mutator is set to the package, via:
3295         // Create:
3296         final long eventId = createAndVerifyEvent(account, seed, calendarId, false, null);
3297         final Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
3298         cursor = mContentResolver.query(uri, new String[] {Events.MUTATORS}, null, null, null);
3299         cursor.moveToFirst();
3300         mutator = cursor.getString(0);
3301         cursor.close();
3302         assertEquals(packageName, mutator);
3303 
3304         // Edit:
3305         // First clear the mutator column
3306         values.clear();
3307         values.putNull(Events.MUTATORS);
3308         mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null);
3309         cursor = mContentResolver.query(uri, new String[] {Events.MUTATORS}, null, null, null);
3310         cursor.moveToFirst();
3311         mutator = cursor.getString(0);
3312         cursor.close();
3313         assertNull(mutator);
3314         // Now edit the event and verify the mutator column
3315         values.clear();
3316         values.put(Events.TITLE, "New title");
3317         mContentResolver.update(uri, values, null, null);
3318         cursor = mContentResolver.query(uri, new String[] {Events.MUTATORS}, null, null, null);
3319         cursor.moveToFirst();
3320         mutator = cursor.getString(0);
3321         cursor.close();
3322         assertEquals(packageName, mutator);
3323 
3324         // Clean up the event
3325         assertEquals(1, EventHelper.deleteEventAsSyncAdapter(mContentResolver, uri, account));
3326 
3327         // Delete:
3328         // First create as sync adapter
3329         final long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, null);
3330         final Uri uri2 = ContentUris.withAppendedId(Events.CONTENT_URI, eventId2);
3331         // Now delete the event and verify
3332         values.clear();
3333         values.put(Events.MUTATORS, packageName);
3334         removeAndVerifyEvent(uri2, values, account);
3335 
3336 
3337         // delete the calendar
3338         removeAndVerifyCalendar(account, calendarId);
3339     }
3340 
3341     /**
3342      * Acquires the set of instances that appear between the specified start and end points.
3343      *
3344      * @param timeZone Time zone to use when parsing startWhen and endWhen
3345      * @param startWhen Start date/time, in RFC 3339 format
3346      * @param endWhen End date/time, in RFC 3339 format
3347      * @param projection Array of desired column names
3348      * @return Cursor with instances (caller should close when done)
3349      */
getInstances(String timeZone, String startWhen, String endWhen, String[] projection, long[] calendarIds)3350     private Cursor getInstances(String timeZone, String startWhen, String endWhen,
3351             String[] projection, long[] calendarIds) {
3352         Time startTime = new Time(timeZone);
3353         startTime.parse3339(startWhen);
3354         long startMillis = startTime.toMillis(false);
3355 
3356         Time endTime = new Time(timeZone);
3357         endTime.parse3339(endWhen);
3358         long endMillis = endTime.toMillis(false);
3359 
3360         // We want a list of instances that occur between the specified dates.  Use the
3361         // "instances/when" URI.
3362         Uri uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_URI,
3363                 startMillis + "/" + endMillis);
3364 
3365         String where = null;
3366         for (int i = 0; i < calendarIds.length; i++) {
3367             if (i > 0) {
3368                 where += " OR ";
3369             } else {
3370                 where = "";
3371             }
3372             where += (Instances.CALENDAR_ID + "=" + calendarIds[i]);
3373         }
3374         Cursor instances = mContentResolver.query(uri, projection, where, null,
3375                 projection[0] + " ASC");
3376 
3377         return instances;
3378     }
3379 
3380     /**
3381      * Acquires the set of instances that appear between the specified start and end points
3382      * that match the search terms.
3383      *
3384      * @param timeZone Time zone to use when parsing startWhen and endWhen
3385      * @param startWhen Start date/time, in RFC 3339 format
3386      * @param endWhen End date/time, in RFC 3339 format
3387      * @param search A collection of tokens to search for.  The columns searched are
3388      *   hard-coded in the provider (currently title, description, location, attendee
3389      *   name, attendee email).
3390      * @param searchByDay If set, adjust start/end to calendar day boundaries.
3391      * @param projection Array of desired column names
3392      * @return Cursor with instances (caller should close when done)
3393      */
getInstancesSearch(String timeZone, String startWhen, String endWhen, String search, boolean searchByDay, String[] projection, long[] calendarIds)3394     private Cursor getInstancesSearch(String timeZone, String startWhen, String endWhen,
3395             String search, boolean searchByDay, String[] projection, long[] calendarIds) {
3396         Time startTime = new Time(timeZone);
3397         startTime.parse3339(startWhen);
3398         long startMillis = startTime.toMillis(false);
3399 
3400         Time endTime = new Time(timeZone);
3401         endTime.parse3339(endWhen);
3402         long endMillis = endTime.toMillis(false);
3403 
3404         Uri uri;
3405         if (searchByDay) {
3406             // start/end are Julian day numbers rather than time in milliseconds
3407             int julianStart = Time.getJulianDay(startMillis, startTime.gmtoff);
3408             int julianEnd = Time.getJulianDay(endMillis, endTime.gmtoff);
3409             uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_SEARCH_BY_DAY_URI,
3410                     julianStart + "/" + julianEnd + "/" + search);
3411         } else {
3412             uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_SEARCH_URI,
3413                     startMillis + "/" + endMillis + "/" + search);
3414         }
3415 
3416         String where = null;
3417         for (int i = 0; i < calendarIds.length; i++) {
3418             if (i > 0) {
3419                 where += " OR ";
3420             } else {
3421                 where = "";
3422             }
3423             where += (Instances.CALENDAR_ID + "=" + calendarIds[i]);
3424         }
3425         // We want a list of instances that occur between the specified dates and that match
3426         // the search terms.
3427 
3428         Cursor instances = mContentResolver.query(uri, projection, where, null,
3429                 projection[0] + " ASC");
3430 
3431         return instances;
3432     }
3433 
3434     /** debug -- dump instances cursor */
dumpInstances(Cursor instances, String timeZone, String msg)3435     private static void dumpInstances(Cursor instances, String timeZone, String msg) {
3436         Log.d(TAG, "Instances (" + msg + ")");
3437 
3438         int posn = instances.getPosition();
3439         instances.moveToPosition(-1);
3440 
3441         //Log.d(TAG, "+++ instances has " + instances.getCount() + " rows, " +
3442         //        instances.getColumnCount() + " columns");
3443         while (instances.moveToNext()) {
3444             long beginMil = instances.getLong(0);
3445             Time beginT = new Time(timeZone);
3446             beginT.set(beginMil);
3447             String logMsg = "--> begin=" + beginT.format3339(false) + " (" + beginMil + ")";
3448             for (int i = 2; i < instances.getColumnCount(); i++) {
3449                 logMsg += " [" + instances.getString(i) + "]";
3450             }
3451             Log.d(TAG, logMsg);
3452         }
3453         instances.moveToPosition(posn);
3454     }
3455 
3456 
3457     /**
3458      * Counts the number of instances that appear between the specified start and end times.
3459      */
getInstanceCount(String timeZone, String startWhen, String endWhen, long[] calendarIds)3460     private int getInstanceCount(String timeZone, String startWhen, String endWhen,
3461                 long[] calendarIds) {
3462         Cursor instances = getInstances(timeZone, startWhen, endWhen,
3463                 new String[] { Instances._ID }, calendarIds);
3464         int count = instances.getCount();
3465         instances.close();
3466         return count;
3467     }
3468 
3469     /**
3470      * Deletes an event as app and sync adapter which removes it from the db and
3471      * verifies after each.
3472      *
3473      * @param eventUri The uri for the event to delete
3474      * @param accountName TODO
3475      */
removeAndVerifyEvent(Uri eventUri, ContentValues eventValues, String accountName)3476     private void removeAndVerifyEvent(Uri eventUri, ContentValues eventValues, String accountName) {
3477         // Delete event
3478         EventHelper.deleteEvent(mContentResolver, eventUri, eventValues);
3479         // Verify
3480         verifyEvent(eventValues, ContentUris.parseId(eventUri));
3481         // Delete as sync adapter
3482         assertEquals(1,
3483                 EventHelper.deleteEventAsSyncAdapter(mContentResolver, eventUri, accountName));
3484         // Verify
3485         Cursor c = EventHelper.getEventByUri(mContentResolver, eventUri);
3486         assertEquals(0, c.getCount());
3487         c.close();
3488     }
3489 
3490     /**
3491      * Creates an event on the given calendar and verifies it.
3492      *
3493      * @param account
3494      * @param seed
3495      * @param calendarId
3496      * @param asSyncAdapter
3497      * @param values optional pre created set of values; will have several new entries added
3498      * @return the _id for the new event
3499      */
createAndVerifyEvent(String account, int seed, long calendarId, boolean asSyncAdapter, ContentValues values)3500     private long createAndVerifyEvent(String account, int seed, long calendarId,
3501             boolean asSyncAdapter, ContentValues values) {
3502         // Create an event
3503         if (values == null) {
3504             values = EventHelper.getNewEventValues(account, seed, calendarId, asSyncAdapter);
3505         }
3506         Uri insertUri = Events.CONTENT_URI;
3507         if (asSyncAdapter) {
3508             insertUri = asSyncAdapter(insertUri, account, CTS_TEST_TYPE);
3509         }
3510         Uri uri = mContentResolver.insert(insertUri, values);
3511         assertNotNull(uri);
3512 
3513         // Verify
3514         EventHelper.addDefaultReadOnlyValues(values, account, asSyncAdapter);
3515         long eventId = ContentUris.parseId(uri);
3516         assertTrue(eventId >= 0);
3517 
3518         verifyEvent(values, eventId);
3519         return eventId;
3520     }
3521 
3522     /**
3523      * Updates an event, and verifies that the updates took.
3524      */
updateAndVerifyEvent(String account, long calendarId, long eventId, boolean asSyncAdapter, ContentValues updateValues)3525     private void updateAndVerifyEvent(String account, long calendarId, long eventId,
3526             boolean asSyncAdapter, ContentValues updateValues) {
3527         Uri uri = Uri.withAppendedPath(Events.CONTENT_URI, String.valueOf(eventId));
3528         if (asSyncAdapter) {
3529             uri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
3530         }
3531         int count = mContentResolver.update(uri, updateValues, null, null);
3532 
3533         // Verify
3534         assertEquals(1, count);
3535         verifyEvent(updateValues, eventId);
3536     }
3537 
3538     /**
3539      * Creates an exception to a recurring event, and verifies it.
3540      * @param account The account to use.
3541      * @param originalEventId The ID of the original event.
3542      * @param values Values for the exception; must include originalInstanceTime.
3543      * @return The _id for the new event.
3544      */
createAndVerifyException(String account, long originalEventId, ContentValues values, boolean asSyncAdapter)3545     private long createAndVerifyException(String account, long originalEventId,
3546             ContentValues values, boolean asSyncAdapter) {
3547         // Create the exception
3548         Uri uri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI,
3549                 String.valueOf(originalEventId));
3550         if (asSyncAdapter) {
3551             uri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
3552         }
3553         Uri resultUri = mContentResolver.insert(uri, values);
3554         assertNotNull(resultUri);
3555         long eventId = ContentUris.parseId(resultUri);
3556         assertTrue(eventId >= 0);
3557         return eventId;
3558     }
3559 
3560     /**
3561      * Deletes an exception to a recurring event.
3562      * @param account The account to use.
3563      * @param eventId The ID of the original recurring event.
3564      * @param excepId The ID of the exception event.
3565      * @return The number of rows deleted.
3566      */
deleteException(String account, long eventId, long excepId)3567     private int deleteException(String account, long eventId, long excepId) {
3568         Uri uri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI,
3569                 eventId + "/" + excepId);
3570         uri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
3571         return mContentResolver.delete(uri, null, null);
3572     }
3573 
3574     /**
3575      * Add some sample attendees to an event.
3576      */
addAttendees(String account, long eventId, int seed)3577     private void addAttendees(String account, long eventId, int seed) {
3578         assertTrue(eventId >= 0);
3579         AttendeeHelper.addAttendee(mContentResolver, eventId,
3580                 "Attender" + seed,
3581                 CalendarHelper.generateCalendarOwnerEmail(account),
3582                 Attendees.ATTENDEE_STATUS_ACCEPTED,
3583                 Attendees.RELATIONSHIP_ORGANIZER,
3584                 Attendees.TYPE_NONE);
3585         seed++;
3586 
3587         AttendeeHelper.addAttendee(mContentResolver, eventId,
3588                 "Attender" + seed,
3589                 "attender" + seed + "@example.com",
3590                 Attendees.ATTENDEE_STATUS_TENTATIVE,
3591                 Attendees.RELATIONSHIP_NONE,
3592                 Attendees.TYPE_NONE);
3593     }
3594 
3595     /**
3596      * Add some sample reminders to an event.
3597      */
addReminders(String account, long eventId, int seed)3598     private void addReminders(String account, long eventId, int seed) {
3599         ReminderHelper.addReminder(mContentResolver, eventId, seed * 5, Reminders.METHOD_ALERT);
3600     }
3601 
3602     /**
3603      * Creates and removes an event that covers a specific range of dates.  Call this to
3604      * cause the provider to expand the CalendarMetaData min/max values to include the range.
3605      * Useful when you want to see the provider expand the instances as the events are added.
3606      */
expandInstanceRange(String account, long calendarId, String testStart, String testEnd, String timeZone)3607     private void expandInstanceRange(String account, long calendarId, String testStart,
3608             String testEnd, String timeZone) {
3609         int seed = 0;
3610 
3611         // TODO: this should use an UNTIL rule based on testEnd, not a COUNT
3612         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed,
3613                 calendarId, true, testStart, "PT1H", "FREQ=DAILY;WKST=SU;COUNT=100");
3614 
3615         /*
3616          * Some of the helper functions modify "eventValues", so we want to make sure we're
3617          * passing a copy of anything we want to re-use.
3618          */
3619         long eventId = createAndVerifyEvent(account, seed, calendarId, true,
3620                 new ContentValues(eventValues));
3621         assertTrue(eventId >= 0);
3622 
3623         String[] projection = { Instances.BEGIN, Instances.START_MINUTE };
3624         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
3625                 new long[] { calendarId });
3626         if (DEBUG_RECURRENCE) {
3627             dumpInstances(instances, timeZone, "prep-create");
3628         }
3629         assertEquals("initial recurrence instance count", 3, instances.getCount());
3630         instances.close();
3631 
3632         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
3633         removeAndVerifyEvent(eventUri, new ContentValues(eventValues), account);
3634 
3635         instances = getInstances(timeZone, testStart, testEnd, projection,
3636                 new long[] { calendarId });
3637         if (DEBUG_RECURRENCE) {
3638             dumpInstances(instances, timeZone, "prep-clear");
3639         }
3640         assertEquals("initial recurrence instance count", 0, instances.getCount());
3641         instances.close();
3642 
3643     }
3644 
3645     /**
3646      * Inserts a new calendar with the given account and seed and verifies it.
3647      *
3648      * @param account The account to add the calendar to
3649      * @param seed A number to use to generate the values
3650      * @return the created calendar's id
3651      */
createAndVerifyCalendar(String account, int seed, ContentValues values)3652     private long createAndVerifyCalendar(String account, int seed, ContentValues values) {
3653         // Create a calendar
3654         if (values == null) {
3655             values = CalendarHelper.getNewCalendarValues(account, seed);
3656         }
3657         Uri syncUri = asSyncAdapter(Calendars.CONTENT_URI, account, CTS_TEST_TYPE);
3658         Uri uri = mContentResolver.insert(syncUri, values);
3659         long calendarId = ContentUris.parseId(uri);
3660         assertTrue(calendarId >= 0);
3661 
3662         verifyCalendar(account, values, calendarId, 1);
3663         return calendarId;
3664     }
3665 
3666     /**
3667      * Deletes a given calendar and verifies no calendars remain on that
3668      * account.
3669      *
3670      * @param account
3671      * @param id
3672      */
removeAndVerifyCalendar(String account, long id)3673     private void removeAndVerifyCalendar(String account, long id) {
3674         // TODO Add code to delete as app and sync adapter and test both
3675 
3676         // Delete
3677         assertEquals(1, CalendarHelper.deleteCalendarById(mContentResolver, id));
3678 
3679         // Verify
3680         Cursor c = CalendarHelper.getCalendarsByAccount(mContentResolver, account);
3681         assertEquals(0, c.getCount());
3682         c.close();
3683     }
3684 
3685     /**
3686      * Check all the fields of a calendar contained in values + id.
3687      *
3688      * @param account the account of the calendar
3689      * @param values the values to check against the db
3690      * @param id the _id of the calendar
3691      * @param expectedCount the number of calendars expected on this account
3692      */
verifyCalendar(String account, ContentValues values, long id, int expectedCount)3693     private void verifyCalendar(String account, ContentValues values, long id, int expectedCount) {
3694         // Verify
3695         Cursor c = CalendarHelper.getCalendarsByAccount(mContentResolver, account);
3696         assertEquals(expectedCount, c.getCount());
3697         assertTrue(c.moveToFirst());
3698         while (c.getLong(0) != id) {
3699             assertTrue(c.moveToNext());
3700         }
3701         for (String key : values.keySet()) {
3702             int index = c.getColumnIndex(key);
3703             assertTrue("Key " + key + " not in projection", index >= 0);
3704             assertEquals(key, values.getAsString(key), c.getString(index));
3705         }
3706         c.close();
3707     }
3708 
3709     /**
3710      * Creates a new _sync_state entry and verifies the contents.
3711      */
createAndVerifySyncState(String account, ContentValues values)3712     private long createAndVerifySyncState(String account, ContentValues values) {
3713         assertNotNull(values);
3714         Uri syncUri = asSyncAdapter(SyncState.CONTENT_URI, account, CTS_TEST_TYPE);
3715         Uri uri = mContentResolver.insert(syncUri, values);
3716         long syncStateId = ContentUris.parseId(uri);
3717         assertTrue(syncStateId >= 0);
3718 
3719         verifySyncState(account, values, syncStateId);
3720         return syncStateId;
3721 
3722     }
3723 
3724     /**
3725      * Removes the _sync_state entry with the specified id, then verifies that it's gone.
3726      */
removeAndVerifySyncState(String account)3727     private void removeAndVerifySyncState(String account) {
3728         assertEquals(1, SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, true));
3729 
3730         // Verify
3731         Cursor c = SyncStateHelper.getSyncStateByAccount(mContentResolver, account);
3732         try {
3733             assertEquals(0, c.getCount());
3734         } finally {
3735             if (c != null) {
3736                 c.close();
3737             }
3738         }
3739     }
3740 
3741     /**
3742      * Check all the fields of a _sync_state entry contained in values + id. This assumes
3743      * a single _sync_state has been created on the given account.
3744      */
verifySyncState(String account, ContentValues values, long id)3745     private void verifySyncState(String account, ContentValues values, long id) {
3746         // Verify
3747         Cursor c = SyncStateHelper.getSyncStateByAccount(mContentResolver, account);
3748         try {
3749             assertEquals(1, c.getCount());
3750             assertTrue(c.moveToFirst());
3751             assertEquals(id, c.getLong(0));
3752             for (String key : values.keySet()) {
3753                 int index = c.getColumnIndex(key);
3754                 if (key.equals(SyncState.DATA)) {
3755                     // TODO: can't compare as string, so compare as byte[]
3756                 } else {
3757                     assertEquals(key, values.getAsString(key), c.getString(index));
3758                 }
3759             }
3760         } finally {
3761             if (c != null) {
3762                 c.close();
3763             }
3764         }
3765     }
3766 
3767 
3768     /**
3769      * Special version of the test runner that does some remote Emma coverage housekeeping.
3770      */
3771     // TODO: find if this is still used and if so convert to AndroidJUnitRunner framework
3772     public static class CalendarEmmaTestRunner extends android.test.InstrumentationTestRunner {
3773         private static final Uri EMMA_CONTENT_URI =
3774             Uri.parse("content://" + CalendarContract.AUTHORITY + "/emma");
3775         private ContentResolver mContentResolver;
3776 
3777         @Override
onStart()3778         public void onStart() {
3779             mContentResolver = getTargetContext().getContentResolver();
3780 
3781             ContentValues values = new ContentValues();
3782             values.put("cmd", "start");
3783             mContentResolver.insert(EMMA_CONTENT_URI, values);
3784 
3785             super.onStart();
3786         }
3787 
3788         @Override
finish(int resultCode, Bundle results)3789         public void finish(int resultCode, Bundle results) {
3790             ContentValues values = new ContentValues();
3791             values.put("cmd", "stop");
3792             values.put("outputFileName",
3793                     Environment.getExternalStorageDirectory() + "/calendar-provider.ec");
3794             mContentResolver.insert(EMMA_CONTENT_URI, values);
3795             super.finish(resultCode, results);
3796         }
3797     }
3798 }
3799