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