1 /*
2  * Copyright (C) 2008 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 com.android.providers.calendar;
18 
19 import android.content.ComponentName;
20 import android.content.ContentProvider;
21 import android.content.ContentResolver;
22 import android.content.ContentUris;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.SharedPreferences;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ProviderInfo;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.database.MatrixCursor;
32 import android.database.sqlite.SQLiteDatabase;
33 import android.database.sqlite.SQLiteOpenHelper;
34 import android.net.Uri;
35 import android.os.UserHandle;
36 import android.provider.BaseColumns;
37 import android.provider.CalendarContract;
38 import android.provider.CalendarContract.Calendars;
39 import android.provider.CalendarContract.Colors;
40 import android.provider.CalendarContract.Events;
41 import android.provider.CalendarContract.Instances;
42 import android.test.AndroidTestCase;
43 import android.test.IsolatedContext;
44 import android.test.RenamingDelegatingContext;
45 import android.test.mock.MockContentResolver;
46 import android.test.mock.MockContext;
47 import android.test.suitebuilder.annotation.SmallTest;
48 import android.test.suitebuilder.annotation.Smoke;
49 import android.test.suitebuilder.annotation.Suppress;
50 import android.text.TextUtils;
51 import android.text.format.DateUtils;
52 import android.text.format.Time;
53 import android.util.Log;
54 
55 import com.android.providers.calendar.enterprise.CrossProfileCalendarHelper;
56 
57 import java.io.File;
58 import java.util.Arrays;
59 import java.util.HashMap;
60 import java.util.Map;
61 import java.util.Set;
62 import java.util.TimeZone;
63 
64 /**
65  * Runs various tests on an isolated Calendar provider with its own database.
66  *
67  * You can run the tests with the following command line:
68  *
69  * adb shell am instrument
70  * -e debug false
71  * -w
72  * -e class com.android.providers.calendar.CalendarProvider2Test
73  * com.android.providers.calendar.tests/android.test.InstrumentationTestRunner
74  *
75  * This test no longer extends ProviderTestCase2 because it actually doesn't
76  * allow you to inject a custom context (which we needed to mock out the calls
77  * to start a service). We the next best thing, which is copy the relevant code
78  * from PTC2 and extend AndroidTestCase instead.
79  */
80 // flaky test, add back to LargeTest when fixed - bug 2395696
81 // @LargeTest
82 public class CalendarProvider2Test extends AndroidTestCase {
83     static final String TAG = "calendar";
84 
85     private static final String DEFAULT_ACCOUNT_TYPE = "com.google";
86     private static final String DEFAULT_ACCOUNT = "joe@joe.com";
87 
88 
89     private static final String WHERE_CALENDARS_SELECTED = Calendars.VISIBLE + "=?";
90     private static final String[] WHERE_CALENDARS_ARGS = {
91         "1"
92     };
93     private static final String WHERE_COLOR_ACCOUNT_AND_INDEX = Colors.ACCOUNT_NAME + "=? AND "
94             + Colors.ACCOUNT_TYPE + "=? AND " + Colors.COLOR_KEY + "=?";
95     private static final String DEFAULT_SORT_ORDER = "begin ASC";
96 
97     private CalendarProvider2ForTesting mProvider;
98     private CalendarProvider2ForTesting mWorkProfileProvider;
99 
100     private SQLiteDatabase mDb;
101     private MetaData mMetaData;
102     private Context mContext;
103     private Context mWorkContext;
104     private MockContentResolver mResolver;
105     private Uri mEventsUri = Events.CONTENT_URI;
106     private Uri mCalendarsUri = Calendars.CONTENT_URI;
107     private int mCalendarId;
108 
109     protected boolean mWipe = false;
110     protected boolean mForceDtend = false;
111 
112     // We need a unique id to put in the _sync_id field so that we can create
113     // recurrence exceptions that refer to recurring events.
114     private int mGlobalSyncId = 1000;
115     private static final String CALENDAR_URL =
116             "http://www.google.com/calendar/feeds/joe%40joe.com/private/full";
117 
118     private static final String TIME_ZONE_AMERICA_ANCHORAGE = "America/Anchorage";
119     private static final String TIME_ZONE_AMERICA_LOS_ANGELES = "America/Los_Angeles";
120     private static final String DEFAULT_TIMEZONE = TIME_ZONE_AMERICA_LOS_ANGELES;
121 
122     private static final String MOCK_TIME_ZONE_DATABASE_VERSION = "2010a";
123 
124     private static final long ONE_MINUTE_MILLIS = 60*1000;
125     private static final long ONE_HOUR_MILLIS = 3600*1000;
126     private static final long ONE_WEEK_MILLIS = 7 * 24 * 3600 * 1000;
127 
128     private static final int WORK_PROFILE_USER_ID = 10;
129     private static final String WORK_PROFILE_AUTHORITY = String.format("%d@%s",
130             WORK_PROFILE_USER_ID, CalendarContract.AUTHORITY);
131 
parseTimeStringToMillis(String timeStr, String timeZone)132     private static long parseTimeStringToMillis(String timeStr, String timeZone) {
133         Time time = new Time(timeZone);
134         time.parse3339(timeStr);
135         return time.toMillis(false /* use isDst */);
136     }
137 
138     private static String WORK_DEFAULT_TIMEZONE = TIME_ZONE_AMERICA_LOS_ANGELES;
139 
140     private static String WORK_CALENDAR_TITLE = "Calendar1";
141     private static String WORK_CALENDAR_TITLE_STANDBY = "Calendar2";
142     private static int WORK_CALENDAR_COLOR = 0xFFFF0000;
143 
144     private static String WORK_EVENT_TITLE = "event_title1";
145     private static String WORK_EVENT_TITLE_STANDBY = "event_title2";
146     private static long WORK_EVENT_DTSTART = parseTimeStringToMillis(
147             "2018-05-01T00:00:00", WORK_DEFAULT_TIMEZONE);
148     private static long WORK_EVENT_DTEND = parseTimeStringToMillis(
149             "2018-05-01T20:00:00", WORK_DEFAULT_TIMEZONE);
150     private final long WORK_EVENT_DTSTART_STANDBY = parseTimeStringToMillis(
151             "2008-05-01T00:00:00", WORK_DEFAULT_TIMEZONE);
152     private final long WORK_EVENT_DTEND_STANDBY = parseTimeStringToMillis(
153             "2008-05-01T20:00:00", WORK_DEFAULT_TIMEZONE);
154     private static int WORK_EVENT_COLOR = 0xff123456;
155     private static String WORK_EVENT_LOCATION = "Work event location.";
156     private static String WORK_EVENT_DESCRIPTION = "This is a work event.";
157 
158     /**
159      * We need a few more stub methods so that our tests can run
160      */
161     protected class MockContext2 extends MockContext {
162 
163         @Override
getPackageName()164         public String getPackageName() {
165             return getContext().getPackageName();
166         }
167 
168         @Override
getResources()169         public Resources getResources() {
170             return getContext().getResources();
171         }
172 
173         @Override
getDir(String name, int mode)174         public File getDir(String name, int mode) {
175             // name the directory so the directory will be seperated from
176             // one created through the regular Context
177             return getContext().getDir("mockcontext2_" + name, mode);
178         }
179 
180         @Override
startService(Intent service)181         public ComponentName startService(Intent service) {
182             return null;
183         }
184 
185         @Override
stopService(Intent service)186         public boolean stopService(Intent service) {
187             return false;
188         }
189 
190         @Override
getPackageManager()191         public PackageManager getPackageManager() {
192             return getContext().getPackageManager();
193         }
194     }
195 
196     /**
197      * KeyValue is a simple class that stores a pair of strings representing
198      * a (key, value) pair.  This is used for updating events.
199      */
200     private class KeyValue {
201         String key;
202         String value;
203 
KeyValue(String key, String value)204         public KeyValue(String key, String value) {
205             this.key = key;
206             this.value = value;
207         }
208     }
209 
210     /**
211      * A generic command interface.  This is used to support a sequence of
212      * commands that can create events, delete or update events, and then
213      * check that the state of the database is as expected.
214      */
215     private interface Command {
execute()216         public void execute();
217     }
218 
219     /**
220      * This is used to insert a new event into the database.  The event is
221      * specified by its name (or "title").  All of the event fields (the
222      * start and end time, whether it is an all-day event, and so on) are
223      * stored in a separate table (the "mEvents" table).
224      */
225     private class Insert implements Command {
226         EventInfo eventInfo;
227 
Insert(String eventName)228         public Insert(String eventName) {
229             eventInfo = findEvent(eventName);
230         }
231 
execute()232         public void execute() {
233             Log.i(TAG, "insert " + eventInfo.mTitle);
234             insertEvent(mCalendarId, eventInfo);
235         }
236     }
237 
238     /**
239      * This is used to delete an event, specified by the event name.
240      */
241     private class Delete implements Command {
242         String eventName;
243         String account;
244         String accountType;
245         int expected;
246 
Delete(String eventName, int expected, String account, String accountType)247         public Delete(String eventName, int expected, String account, String accountType) {
248             this.eventName = eventName;
249             this.expected = expected;
250             this.account = account;
251             this.accountType = accountType;
252         }
253 
execute()254         public void execute() {
255             Log.i(TAG, "delete " + eventName);
256             int rows = deleteMatchingEvents(eventName, account, accountType);
257             assertEquals(expected, rows);
258         }
259     }
260 
261     /**
262      * This is used to update an event.  The values to update are specified
263      * with an array of (key, value) pairs.  Both the key and value are
264      * specified as strings.  Event fields that are not really strings (such
265      * as DTSTART which is a long) should be converted to the appropriate type
266      * but that isn't supported yet.  When needed, that can be added here
267      * by checking for specific keys and converting the associated values.
268      */
269     private class Update implements Command {
270         String eventName;
271         KeyValue[] pairs;
272 
Update(String eventName, KeyValue[] pairs)273         public Update(String eventName, KeyValue[] pairs) {
274             this.eventName = eventName;
275             this.pairs = pairs;
276         }
277 
execute()278         public void execute() {
279             Log.i(TAG, "update " + eventName);
280             if (mWipe) {
281                 // Wipe instance table so it will be regenerated
282                 mMetaData.clearInstanceRange();
283             }
284             ContentValues map = new ContentValues();
285             for (KeyValue pair : pairs) {
286                 String value = pair.value;
287                 if (CalendarContract.Events.STATUS.equals(pair.key)) {
288                     // Do type conversion for STATUS
289                     map.put(pair.key, Integer.parseInt(value));
290                 } else {
291                     map.put(pair.key, value);
292                 }
293             }
294             if (map.size() == 1 && map.containsKey(Events.STATUS)) {
295                 updateMatchingEventsStatusOnly(eventName, map);
296             } else {
297                 updateMatchingEvents(eventName, map);
298             }
299         }
300     }
301 
302     /**
303      * This command queries the number of events and compares it to the given
304      * expected value.
305      */
306     private class QueryNumEvents implements Command {
307         int expected;
308 
QueryNumEvents(int expected)309         public QueryNumEvents(int expected) {
310             this.expected = expected;
311         }
312 
execute()313         public void execute() {
314             Cursor cursor = mResolver.query(mEventsUri, null, null, null, null);
315             assertEquals(expected, cursor.getCount());
316             cursor.close();
317         }
318     }
319 
320 
321     /**
322      * This command dumps the list of events to the log for debugging.
323      */
324     private class DumpEvents implements Command {
325 
DumpEvents()326         public DumpEvents() {
327         }
328 
execute()329         public void execute() {
330             Cursor cursor = mResolver.query(mEventsUri, null, null, null, null);
331             dumpCursor(cursor);
332             cursor.close();
333         }
334     }
335 
336     /**
337      * This command dumps the list of instances to the log for debugging.
338      */
339     private class DumpInstances implements Command {
340         long begin;
341         long end;
342 
DumpInstances(String startDate, String endDate)343         public DumpInstances(String startDate, String endDate) {
344             Time time = new Time(DEFAULT_TIMEZONE);
345             time.parse3339(startDate);
346             begin = time.toMillis(false /* use isDst */);
347             time.parse3339(endDate);
348             end = time.toMillis(false /* use isDst */);
349         }
350 
execute()351         public void execute() {
352             Cursor cursor = queryInstances(begin, end);
353             dumpCursor(cursor);
354             cursor.close();
355         }
356     }
357 
358     /**
359      * This command queries the number of instances and compares it to the given
360      * expected value.
361      */
362     private class QueryNumInstances implements Command {
363         int expected;
364         long begin;
365         long end;
366 
QueryNumInstances(String startDate, String endDate, int expected)367         public QueryNumInstances(String startDate, String endDate, int expected) {
368             Time time = new Time(DEFAULT_TIMEZONE);
369             time.parse3339(startDate);
370             begin = time.toMillis(false /* use isDst */);
371             time.parse3339(endDate);
372             end = time.toMillis(false /* use isDst */);
373             this.expected = expected;
374         }
375 
execute()376         public void execute() {
377             Cursor cursor = queryInstances(begin, end);
378             assertEquals(expected, cursor.getCount());
379             cursor.close();
380         }
381     }
382 
383     /**
384      * When this command runs it verifies that all of the instances in the
385      * given range match the expected instances (each instance is specified by
386      * a start date).
387      * If you just want to verify that an instance exists in a given date
388      * range, use {@link VerifyInstance} instead.
389      */
390     private class VerifyAllInstances implements Command {
391         long[] instances;
392         long begin;
393         long end;
394 
VerifyAllInstances(String startDate, String endDate, String[] dates)395         public VerifyAllInstances(String startDate, String endDate, String[] dates) {
396             Time time = new Time(DEFAULT_TIMEZONE);
397             time.parse3339(startDate);
398             begin = time.toMillis(false /* use isDst */);
399             time.parse3339(endDate);
400             end = time.toMillis(false /* use isDst */);
401 
402             if (dates == null) {
403                 return;
404             }
405 
406             // Convert all the instance date strings to UTC milliseconds
407             int len = dates.length;
408             this.instances = new long[len];
409             int index = 0;
410             for (String instance : dates) {
411                 time.parse3339(instance);
412                 this.instances[index++] = time.toMillis(false /* use isDst */);
413             }
414         }
415 
execute()416         public void execute() {
417             Cursor cursor = queryInstances(begin, end);
418             int len = 0;
419             if (instances != null) {
420                 len = instances.length;
421             }
422             if (len != cursor.getCount()) {
423                 dumpCursor(cursor);
424             }
425             assertEquals("number of instances don't match", len, cursor.getCount());
426 
427             if (instances == null) {
428                 return;
429             }
430 
431             int beginColumn = cursor.getColumnIndex(Instances.BEGIN);
432             while (cursor.moveToNext()) {
433                 long begin = cursor.getLong(beginColumn);
434 
435                 // Search the list of expected instances for a matching start
436                 // time.
437                 boolean found = false;
438                 for (long instance : instances) {
439                     if (instance == begin) {
440                         found = true;
441                         break;
442                     }
443                 }
444                 if (!found) {
445                     int titleColumn = cursor.getColumnIndex(Events.TITLE);
446                     int allDayColumn = cursor.getColumnIndex(Events.ALL_DAY);
447 
448                     String title = cursor.getString(titleColumn);
449                     boolean allDay = cursor.getInt(allDayColumn) != 0;
450                     int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE |
451                             DateUtils.FORMAT_24HOUR;
452                     if (allDay) {
453                         flags |= DateUtils.FORMAT_UTC;
454                     } else {
455                         flags |= DateUtils.FORMAT_SHOW_TIME;
456                     }
457                     String date = DateUtils.formatDateRange(mContext, begin, begin, flags);
458                     String mesg = String.format("Test failed!"
459                             + " unexpected instance (\"%s\") at %s",
460                             title, date);
461                     Log.e(TAG, mesg);
462                 }
463                 if (!found) {
464                     dumpCursor(cursor);
465                 }
466                 assertTrue(found);
467             }
468             cursor.close();
469         }
470     }
471 
472     /**
473      * When this command runs it verifies that the given instance exists in
474      * the given date range.
475      */
476     private class VerifyInstance implements Command {
477         long instance;
478         boolean allDay;
479         long begin;
480         long end;
481 
482         /**
483          * Creates a command to check that the given range [startDate,endDate]
484          * contains a specific instance of an event (specified by "date").
485          *
486          * @param startDate the beginning of the date range
487          * @param endDate the end of the date range
488          * @param date the date or date-time string of an event instance
489          */
VerifyInstance(String startDate, String endDate, String date)490         public VerifyInstance(String startDate, String endDate, String date) {
491             Time time = new Time(DEFAULT_TIMEZONE);
492             time.parse3339(startDate);
493             begin = time.toMillis(false /* use isDst */);
494             time.parse3339(endDate);
495             end = time.toMillis(false /* use isDst */);
496 
497             // Convert the instance date string to UTC milliseconds
498             time.parse3339(date);
499             allDay = time.allDay;
500             instance = time.toMillis(false /* use isDst */);
501         }
502 
execute()503         public void execute() {
504             Cursor cursor = queryInstances(begin, end);
505             int beginColumn = cursor.getColumnIndex(Instances.BEGIN);
506             boolean found = false;
507             while (cursor.moveToNext()) {
508                 long begin = cursor.getLong(beginColumn);
509 
510                 if (instance == begin) {
511                     found = true;
512                     break;
513                 }
514             }
515             if (!found) {
516                 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE;
517                 if (allDay) {
518                     flags |= DateUtils.FORMAT_UTC;
519                 } else {
520                     flags |= DateUtils.FORMAT_SHOW_TIME;
521                 }
522                 String date = DateUtils.formatDateRange(mContext, instance, instance, flags);
523                 String mesg = String.format("Test failed!"
524                         + " cannot find instance at %s",
525                         date);
526                 Log.e(TAG, mesg);
527             }
528             assertTrue(found);
529             cursor.close();
530         }
531     }
532 
533     /**
534      * This class stores all the useful information about an event.
535      */
536     private class EventInfo {
537         String mTitle;
538         String mDescription;
539         String mTimezone;
540         boolean mAllDay;
541         long mDtstart;
542         long mDtend;
543         String mRrule;
544         String mDuration;
545         String mOriginalTitle;
546         long mOriginalInstance;
547         int mSyncId;
548         String mCustomAppPackage;
549         String mCustomAppUri;
550         String mUid2445;
551 
552         // Constructor for normal events, using the default timezone
EventInfo(String title, String startDate, String endDate, boolean allDay)553         public EventInfo(String title, String startDate, String endDate,
554                 boolean allDay) {
555             init(title, startDate, endDate, allDay, DEFAULT_TIMEZONE);
556         }
557 
558         // Constructor for normal events, specifying the timezone
EventInfo(String title, String startDate, String endDate, boolean allDay, String timezone)559         public EventInfo(String title, String startDate, String endDate,
560                 boolean allDay, String timezone) {
561             init(title, startDate, endDate, allDay, timezone);
562         }
563 
init(String title, String startDate, String endDate, boolean allDay, String timezone)564         public void init(String title, String startDate, String endDate,
565                 boolean allDay, String timezone) {
566             mTitle = title;
567             Time time = new Time();
568             if (allDay) {
569                 time.timezone = Time.TIMEZONE_UTC;
570             } else if (timezone != null) {
571                 time.timezone = timezone;
572             }
573             mTimezone = time.timezone;
574             time.parse3339(startDate);
575             mDtstart = time.toMillis(false /* use isDst */);
576             time.parse3339(endDate);
577             mDtend = time.toMillis(false /* use isDst */);
578             mDuration = null;
579             mRrule = null;
580             mAllDay = allDay;
581             mCustomAppPackage = "CustomAppPackage-" + mTitle;
582             mCustomAppUri = "CustomAppUri-" + mTitle;
583             mUid2445 = null;
584         }
585 
586         // Constructor for repeating events, using the default timezone
EventInfo(String title, String description, String startDate, String endDate, String rrule, boolean allDay)587         public EventInfo(String title, String description, String startDate, String endDate,
588                 String rrule, boolean allDay) {
589             init(title, description, startDate, endDate, rrule, allDay, DEFAULT_TIMEZONE);
590         }
591 
592         // Constructor for repeating events, specifying the timezone
EventInfo(String title, String description, String startDate, String endDate, String rrule, boolean allDay, String timezone)593         public EventInfo(String title, String description, String startDate, String endDate,
594                 String rrule, boolean allDay, String timezone) {
595             init(title, description, startDate, endDate, rrule, allDay, timezone);
596         }
597 
init(String title, String description, String startDate, String endDate, String rrule, boolean allDay, String timezone)598         public void init(String title, String description, String startDate, String endDate,
599                 String rrule, boolean allDay, String timezone) {
600             mTitle = title;
601             mDescription = description;
602             Time time = new Time();
603             if (allDay) {
604                 time.timezone = Time.TIMEZONE_UTC;
605             } else if (timezone != null) {
606                 time.timezone = timezone;
607             }
608             mTimezone = time.timezone;
609             time.parse3339(startDate);
610             mDtstart = time.toMillis(false /* use isDst */);
611             if (endDate != null) {
612                 time.parse3339(endDate);
613                 mDtend = time.toMillis(false /* use isDst */);
614             }
615             if (allDay) {
616                 long days = 1;
617                 if (endDate != null) {
618                     days = (mDtend - mDtstart) / DateUtils.DAY_IN_MILLIS;
619                 }
620                 mDuration = "P" + days + "D";
621             } else {
622                 long seconds = (mDtend - mDtstart) / DateUtils.SECOND_IN_MILLIS;
623                 mDuration = "P" + seconds + "S";
624             }
625             mRrule = rrule;
626             mAllDay = allDay;
627         }
628 
629         // Constructor for recurrence exceptions, using the default timezone
EventInfo(String originalTitle, String originalInstance, String title, String description, String startDate, String endDate, boolean allDay, String customPackageName, String customPackageUri, String mUid2445)630         public EventInfo(String originalTitle, String originalInstance, String title,
631                 String description, String startDate, String endDate, boolean allDay,
632                 String customPackageName, String customPackageUri, String mUid2445) {
633             init(originalTitle, originalInstance,
634                     title, description, startDate, endDate, allDay, DEFAULT_TIMEZONE,
635                     customPackageName, customPackageUri, mUid2445);
636         }
637 
init(String originalTitle, String originalInstance, String title, String description, String startDate, String endDate, boolean allDay, String timezone, String customPackageName, String customPackageUri, String uid2445)638         public void init(String originalTitle, String originalInstance,
639                 String title, String description, String startDate, String endDate,
640                 boolean allDay, String timezone, String customPackageName,
641                 String customPackageUri, String uid2445) {
642             mOriginalTitle = originalTitle;
643             Time time = new Time(timezone);
644             time.parse3339(originalInstance);
645             mOriginalInstance = time.toMillis(false /* use isDst */);
646             mCustomAppPackage = customPackageName;
647             mCustomAppUri = customPackageUri;
648             mUid2445 = uid2445;
649             init(title, description, startDate, endDate, null /* rrule */, allDay, timezone);
650         }
651     }
652 
653     private class InstanceInfo {
654         EventInfo mEvent;
655         long mBegin;
656         long mEnd;
657         int mExpectedOccurrences;
658 
InstanceInfo(String eventName, String startDate, String endDate, int expected)659         public InstanceInfo(String eventName, String startDate, String endDate, int expected) {
660             // Find the test index that contains the given event name
661             mEvent = findEvent(eventName);
662             Time time = new Time(mEvent.mTimezone);
663             time.parse3339(startDate);
664             mBegin = time.toMillis(false /* use isDst */);
665             time.parse3339(endDate);
666             mEnd = time.toMillis(false /* use isDst */);
667             mExpectedOccurrences = expected;
668         }
669     }
670 
671     /**
672      * This is the main table of events.  The events in this table are
673      * referred to by name in other places.
674      */
675     private EventInfo[] mEvents = {
676             new EventInfo("normal0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", false),
677             new EventInfo("normal1", "2008-05-26T08:30:00", "2008-05-26T09:30:00", false),
678             new EventInfo("normal2", "2008-05-26T14:30:00", "2008-05-26T15:30:00", false),
679             new EventInfo("allday0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", true),
680             new EventInfo("allday1", "2008-05-02T00:00:00", "2008-05-31T00:00:00", true),
681             new EventInfo("daily0", "daily from 5/1/2008 12am to 1am",
682                     "2008-05-01T00:00:00", "2008-05-01T01:00:00",
683                     "FREQ=DAILY;WKST=SU", false),
684             new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am",
685                     "2008-05-01T08:30:00", "2008-05-01T09:30:00",
686                     "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false),
687             new EventInfo("daily2", "daily from 5/1/2008 8:45am to 9:15am until 5/3/2008 10am",
688                     "2008-05-01T08:45:00", "2008-05-01T09:15:00",
689                     "FREQ=DAILY;UNTIL=20080503T170000Z;WKST=SU", false),
690             new EventInfo("allday daily0", "all-day daily from 5/1/2008",
691                     "2008-05-01", null,
692                     "FREQ=DAILY;WKST=SU", true),
693             new EventInfo("allday daily1", "all-day daily from 5/1/2008 until 5/3/2008",
694                     "2008-05-01", null,
695                     "FREQ=DAILY;UNTIL=20080503T000000Z;WKST=SU", true),
696             new EventInfo("allday weekly0", "all-day weekly from 5/1/2008",
697                     "2008-05-01", null,
698                     "FREQ=WEEKLY;WKST=SU", true),
699             new EventInfo("allday weekly1", "all-day for 2 days weekly from 5/1/2008",
700                     "2008-05-01", "2008-05-03",
701                     "FREQ=WEEKLY;WKST=SU", true),
702             new EventInfo("allday yearly0", "all-day yearly on 5/1/2008",
703                     "2008-05-01T", null,
704                     "FREQ=YEARLY;WKST=SU", true),
705             new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm",
706                     "2008-05-06T13:00:00", "2008-05-06T14:00:00",
707                     "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false),
708             new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 2:30pm to 3:30pm",
709                     "2008-05-06T14:30:00", "2008-05-06T15:30:00",
710                     "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false),
711             new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm",
712                     "2008-05-20T15:00:00", "2008-05-20T16:00:00",
713                     "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false),
714             new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 12:00am to 12:10am",
715                     "2008-05-01T00:00:00", "2008-05-01T00:10:00",
716                     "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false),
717             new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight",
718                     "2008-05-31T23:00:00", "2008-06-01T00:00:00",
719                     "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false),
720             new EventInfo("daily0", "2008-05-01T00:00:00",
721                     "except0", "daily0 exception for 5/1/2008 12am, change to 5/1/2008 2am to 3am",
722                     "2008-05-01T02:00:00", "2008-05-01T01:03:00", false, "AppPkg1", "AppUri1",
723                     "uid2445-1"),
724             new EventInfo("daily0", "2008-05-03T00:00:00",
725                     "except1", "daily0 exception for 5/3/2008 12am, change to 5/3/2008 2am to 3am",
726                     "2008-05-03T02:00:00", "2008-05-03T01:03:00", false, "AppPkg2", "AppUri2",
727                     null),
728             new EventInfo("daily0", "2008-05-02T00:00:00",
729                     "except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008",
730                     "2008-01-02T00:00:00", "2008-01-02T01:00:00", false, "AppPkg3", "AppUri3",
731                     "12345@uid2445"),
732             new EventInfo("weekly0", "2008-05-13T13:00:00",
733                     "except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm",
734                     "2008-12-11T13:00:00", "2008-12-11T14:00:00", false, "AppPkg4", "AppUri4",
735                     null),
736             new EventInfo("weekly0", "2008-05-13T13:00:00",
737                     "cancel0", "weekly0 exception for 5/13/2008 1pm",
738                     "2008-05-13T13:00:00", "2008-05-13T14:00:00", false, "AppPkg5", "AppUri5",
739                     null),
740             new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm",
741                     "2008-05-01T13:00:00", "2008-05-01T14:00:00",
742                     "FREQ=YEARLY;WKST=SU", false),
743     };
744 
745     /**
746      * This table is used to verify the events generated by mEvents.  It checks that the
747      * number of instances within a given range matches the expected number
748      * of instances.
749      */
750     private InstanceInfo[] mInstanceRanges = {
751             new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T00:01:00", 1),
752             new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1),
753             new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 2),
754             new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T23:59:00", 2),
755             new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T00:01:00", 1),
756             new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T01:00:00", 1),
757             new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", 2),
758             new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 31),
759             new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-06-01T23:59:00", 32),
760 
761             new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1),
762             new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 2),
763 
764             new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1),
765             new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 3),
766 
767             new InstanceInfo("allday daily0", "2008-05-01", "2008-05-07", 7),
768             new InstanceInfo("allday daily1", "2008-05-01", "2008-05-07", 3),
769             new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-07", 1),
770             new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-08", 2),
771             new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-31", 5),
772             new InstanceInfo("allday weekly1", "2008-05-01", "2008-05-31", 5),
773             new InstanceInfo("allday yearly0", "2008-05-01", "2009-04-30", 1),
774             new InstanceInfo("allday yearly0", "2008-05-01", "2009-05-02", 2),
775 
776             new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0),
777             new InstanceInfo("weekly0", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1),
778             new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 4),
779             new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 8),
780 
781             new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0),
782             new InstanceInfo("weekly1", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1),
783             new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 2),
784             new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 4),
785 
786             new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T13:00:00", 0),
787             new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T15:00:00", 1),
788             new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-05-31T00:00:00", 0),
789             new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T14:59:00", 0),
790             new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T15:00:00", 1),
791             new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1),
792             new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 2),
793 
794             new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1),
795             new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1),
796             new InstanceInfo("monthly1", "2008-05-01T00:10:00", "2008-05-31T23:59:00", 1),
797             new InstanceInfo("monthly1", "2008-05-01T00:11:00", "2008-05-31T23:59:00", 0),
798             new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-06-01T00:00:00", 2),
799 
800             new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 0),
801             new InstanceInfo("monthly2", "2008-05-01T00:10:00", "2008-05-31T23:00:00", 1),
802             new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-07-01T00:00:00", 1),
803             new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-08-01T00:00:00", 2),
804 
805             new InstanceInfo("yearly0", "2008-05-01", "2009-04-30", 1),
806             new InstanceInfo("yearly0", "2008-05-01", "2009-05-02", 2),
807     };
808 
809     /**
810      * This sequence of commands inserts and deletes some events.
811      */
812     private Command[] mNormalInsertDelete = {
813             new Insert("normal0"),
814             new Insert("normal1"),
815             new Insert("normal2"),
816             new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 3),
817             new Delete("normal1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
818             new QueryNumEvents(2),
819             new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 2),
820             new Delete("normal1", 0, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
821             new Delete("normal2", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
822             new QueryNumEvents(1),
823             new Delete("normal0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
824             new QueryNumEvents(0),
825     };
826 
827     /**
828      * This sequence of commands inserts and deletes some all-day events.
829      */
830     private Command[] mAlldayInsertDelete = {
831             new Insert("allday0"),
832             new Insert("allday1"),
833             new QueryNumEvents(2),
834             new QueryNumInstances("2008-05-01T00:00:00", "2008-05-01T00:01:00", 0),
835             new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 2),
836             new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1),
837             new Delete("allday0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
838             new QueryNumEvents(1),
839             new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 1),
840             new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1),
841             new Delete("allday1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
842             new QueryNumEvents(0),
843     };
844 
845     /**
846      * This sequence of commands inserts and deletes some repeating events.
847      */
848     private Command[] mRecurringInsertDelete = {
849             new Insert("daily0"),
850             new Insert("daily1"),
851             new QueryNumEvents(2),
852             new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 3),
853             new QueryNumInstances("2008-05-01T01:01:00", "2008-05-02T00:01:00", 2),
854             new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 6),
855             new Delete("daily1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
856             new QueryNumEvents(1),
857             new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 2),
858             new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 4),
859             new Delete("daily0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
860             new QueryNumEvents(0),
861     };
862 
863     /**
864      * This sequence of commands creates a recurring event with a recurrence
865      * exception that moves an event outside the expansion window.  It checks that the
866      * recurrence exception does not occur in the Instances database table.
867      * Bug 1642665
868      */
869     private Command[] mExceptionWithMovedRecurrence = {
870             new Insert("daily0"),
871             new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
872                     new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
873                             "2008-05-03T00:00:00", }),
874             new Insert("except2"),
875             new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
876                     new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}),
877     };
878 
879     /**
880      * This sequence of commands deletes (cancels) one instance of a recurrence.
881      */
882     private Command[] mCancelInstance = {
883             new Insert("weekly0"),
884             new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
885                     new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00",
886                             "2008-05-20T13:00:00", }),
887             new Insert("cancel0"),
888             new Update("cancel0", new KeyValue[] {
889                     new KeyValue(CalendarContract.Events.STATUS,
890                         Integer.toString(CalendarContract.Events.STATUS_CANCELED)),
891             }),
892             new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
893                     new String[] {"2008-05-06T13:00:00",
894                             "2008-05-20T13:00:00", }),
895     };
896     /**
897      * This sequence of commands creates a recurring event with a recurrence
898      * exception that moves an event from outside the expansion window into the
899      * expansion window.
900      */
901     private Command[] mExceptionWithMovedRecurrence2 = {
902             new Insert("weekly0"),
903             new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
904                     new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
905                             "2008-12-16T13:00:00", }),
906             new Insert("except3"),
907             new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
908                     new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
909                             "2008-12-11T13:00:00", "2008-12-16T13:00:00", }),
910     };
911     /**
912      * This sequence of commands creates a recurring event with a recurrence
913      * exception and then changes the end time of the recurring event.  It then
914      * checks that the recurrence exception does not occur in the Instances
915      * database table.
916      */
917     private Command[]
918             mExceptionWithTruncatedRecurrence = {
919             new Insert("daily0"),
920             // Verify 4 occurrences of the "daily0" repeating event
921             new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
922                     new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
923                             "2008-05-03T00:00:00", "2008-05-04T00:00:00"}),
924             new Insert("except1"),
925             new QueryNumEvents(2),
926 
927             // Verify that one of the 4 occurrences has its start time changed
928             // so that it now matches the recurrence exception.
929             new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
930                     new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
931                             "2008-05-03T02:00:00", "2008-05-04T00:00:00"}),
932 
933             // Change the end time of "daily0" but it still includes the
934             // recurrence exception.
935             new Update("daily0", new KeyValue[] {
936                     new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080505T150000Z;WKST=SU"),
937             }),
938 
939             // Verify that the recurrence exception is still there
940             new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
941                     new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
942                             "2008-05-03T02:00:00", "2008-05-04T00:00:00"}),
943             // This time change the end time of "daily0" so that it excludes
944             // the recurrence exception.
945             new Update("daily0", new KeyValue[] {
946                     new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"),
947             }),
948             // The server will cancel the out-of-range exception.
949             // It would be nice for the provider to handle this automatically,
950             // but for now simulate the server-side cancel.
951             new Update("except1", new KeyValue[] {
952                 new KeyValue(CalendarContract.Events.STATUS,
953                         Integer.toString(CalendarContract.Events.STATUS_CANCELED)),
954             }),
955             // Verify that the recurrence exception does not appear.
956             new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
957                     new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}),
958     };
959 
960     /**
961      * Bug 135848.  Ensure that a recurrence exception is displayed even if the recurrence
962      * is not present.
963      */
964     private Command[] mExceptionWithNoRecurrence = {
965             new Insert("except0"),
966             new QueryNumEvents(1),
967             new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
968                     new String[] {"2008-05-01T02:00:00"}),
969     };
970 
findEvent(String name)971     private EventInfo findEvent(String name) {
972         int len = mEvents.length;
973         for (int ii = 0; ii < len; ii++) {
974             EventInfo event = mEvents[ii];
975             if (name.equals(event.mTitle)) {
976                 return event;
977             }
978         }
979         return null;
980     }
981 
982     @Override
setUp()983     protected void setUp() throws Exception {
984         super.setUp();
985         // This code here is the code that was originally in ProviderTestCase2
986         mResolver = new MockContentResolver();
987 
988         final String filenamePrefix = "test.";
989         RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(
990                 new MockContext2(), // The context that most methods are delegated to
991                 getContext(), // The context that file methods are delegated to
992                 filenamePrefix) {
993             @Override
994             public SharedPreferences getSharedPreferences(String name, int mode) {
995                 return getContext().getSharedPreferences(name, mode);
996             }
997         };
998         mContext = new IsolatedContext(mResolver, targetContextWrapper) {
999             @Override
1000             public Object getSystemService(String name) {
1001                 switch (name) {
1002                     case Context.POWER_SERVICE:
1003                     case Context.USER_SERVICE:
1004                         return getContext().getSystemService(name);
1005                     default:
1006                         return super.getSystemService(name);
1007                 }
1008             }
1009         };
1010 
1011         mWorkContext = new IsolatedContext(mResolver, targetContextWrapper) {
1012             @Override
1013             public int getUserId() {
1014                 return WORK_PROFILE_USER_ID;
1015             }
1016 
1017             @Override
1018             public Object getSystemService(String name) {
1019                 switch (name) {
1020                     case Context.POWER_SERVICE:
1021                     case Context.USER_SERVICE:
1022                         return getContext().getSystemService(name);
1023                     default:
1024                         return super.getSystemService(name);
1025                 }
1026             }
1027         };
1028 
1029         mProvider = new CalendarProvider2ForTesting() {
1030             @Override
1031             protected int getWorkProfileUserId() {
1032                 return WORK_PROFILE_USER_ID;
1033             }
1034 
1035             @Override
1036             protected int getParentUserId() {
1037                 return UserHandle.USER_NULL;
1038             }
1039 
1040             @Override
1041             protected void initCrossProfileCalendarHelper() {
1042                 mCrossProfileCalendarHelper = new MockCrossProfileCalendarHelper(mContext);
1043             }
1044         };
1045         ProviderInfo info = new ProviderInfo();
1046         info.authority = CalendarContract.AUTHORITY;
1047         mProvider.attachInfoForTesting(mContext, info);
1048 
1049         mWorkProfileProvider = new CalendarProvider2ForTesting() {
1050             @Override
1051             protected int getWorkProfileUserId() {
1052                 return UserHandle.USER_NULL;
1053             }
1054 
1055             @Override
1056             protected int getParentUserId() {
1057                 return UserHandle.myUserId();
1058             }
1059 
1060             @Override
1061             protected void initCrossProfileCalendarHelper() {
1062                 mCrossProfileCalendarHelper = new MockCrossProfileCalendarHelper(mContext);
1063             }
1064         };
1065         ProviderInfo workProviderInfo = new ProviderInfo();
1066         workProviderInfo.authority = WORK_PROFILE_AUTHORITY;
1067         mWorkProfileProvider.attachInfoForTesting(mWorkContext, info);
1068 
1069         mResolver.addProvider(CalendarContract.AUTHORITY, mProvider);
1070         mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds"));
1071         mResolver.addProvider("sync", new MockProvider("sync"));
1072         mResolver.addProvider(WORK_PROFILE_AUTHORITY, mWorkProfileProvider);
1073 
1074         mMetaData = getProvider().mMetaData;
1075         mForceDtend = false;
1076 
1077         CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
1078         mDb = helper.getWritableDatabase();
1079         wipeAndInitData(helper, mDb);
1080     }
1081 
1082     @Override
tearDown()1083     protected void tearDown() throws Exception {
1084         try {
1085             mDb.close();
1086             mDb = null;
1087             getProvider().getDatabaseHelper().close();
1088         } catch (IllegalStateException e) {
1089             e.printStackTrace();
1090         }
1091         super.tearDown();
1092     }
1093 
wipeAndInitData(SQLiteOpenHelper helper, SQLiteDatabase db)1094     public void wipeAndInitData(SQLiteOpenHelper helper, SQLiteDatabase db)
1095             throws CalendarCache.CacheException {
1096         db.beginTransaction();
1097 
1098         // Clean tables
1099         db.delete("Calendars", null, null);
1100         db.delete("Events", null, null);
1101         db.delete("EventsRawTimes", null, null);
1102         db.delete("Instances", null, null);
1103         db.delete("CalendarMetaData", null, null);
1104         db.delete("CalendarCache", null, null);
1105         db.delete("Attendees", null, null);
1106         db.delete("Reminders", null, null);
1107         db.delete("CalendarAlerts", null, null);
1108         db.delete("ExtendedProperties", null, null);
1109 
1110         // Set CalendarCache data
1111         initCalendarCacheLocked(helper, db);
1112 
1113         // set CalendarMetaData data
1114         long now = System.currentTimeMillis();
1115         ContentValues values = new ContentValues();
1116         values.put("localTimezone", "America/Los_Angeles");
1117         values.put("minInstance", 1207008000000L); // 1st April 2008
1118         values.put("maxInstance", now + ONE_WEEK_MILLIS);
1119         db.insert("CalendarMetaData", null, values);
1120 
1121         db.setTransactionSuccessful();
1122         db.endTransaction();
1123     }
1124 
initCalendarCacheLocked(SQLiteOpenHelper helper, SQLiteDatabase db)1125     private void initCalendarCacheLocked(SQLiteOpenHelper helper, SQLiteDatabase db)
1126             throws CalendarCache.CacheException {
1127         CalendarCache cache = new CalendarCache(helper);
1128 
1129         String localTimezone = TimeZone.getDefault().getID();
1130 
1131         // Set initial values
1132         cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_DATABASE_VERSION, "2010k");
1133         cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_TYPE, CalendarCache.TIMEZONE_TYPE_AUTO);
1134         cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES, localTimezone);
1135         cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS, localTimezone);
1136     }
1137 
getProvider()1138     protected CalendarProvider2ForTesting getProvider() {
1139         return mProvider;
1140     }
1141 
1142     /**
1143      * Dumps the contents of the given cursor to the log.  For debugging.
1144      * @param cursor the database cursor
1145      */
dumpCursor(Cursor cursor)1146     private void dumpCursor(Cursor cursor) {
1147         cursor.moveToPosition(-1);
1148         String[] cols = cursor.getColumnNames();
1149 
1150         Log.i(TAG, "dumpCursor() count: " + cursor.getCount());
1151         int index = 0;
1152         while (cursor.moveToNext()) {
1153             Log.i(TAG, index + " {");
1154             for (int i = 0; i < cols.length; i++) {
1155                 Log.i(TAG, "    " + cols[i] + '=' + cursor.getString(i));
1156             }
1157             Log.i(TAG, "}");
1158             index += 1;
1159         }
1160         cursor.moveToPosition(-1);
1161     }
1162 
insertCal(String name, String timezone)1163     private int insertCal(String name, String timezone) {
1164         return insertCal(name, timezone, DEFAULT_ACCOUNT);
1165     }
1166 
1167     /**
1168      * Creates a new calendar, with the provided name, time zone, and account name.
1169      *
1170      * @return the new calendar's _ID value
1171      */
insertCal(String name, String timezone, String account)1172     private int insertCal(String name, String timezone, String account) {
1173         ContentValues m = new ContentValues();
1174         m.put(Calendars.NAME, name);
1175         m.put(Calendars.CALENDAR_DISPLAY_NAME, name);
1176         m.put(Calendars.CALENDAR_COLOR, 0xff123456);
1177         m.put(Calendars.CALENDAR_TIME_ZONE, timezone);
1178         m.put(Calendars.VISIBLE, 1);
1179         m.put(Calendars.CAL_SYNC1, CALENDAR_URL);
1180         m.put(Calendars.OWNER_ACCOUNT, account);
1181         m.put(Calendars.ACCOUNT_NAME,  account);
1182         m.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
1183         m.put(Calendars.SYNC_EVENTS,  1);
1184 
1185         Uri url = mResolver.insert(
1186                 addSyncQueryParams(mCalendarsUri, account, DEFAULT_ACCOUNT_TYPE), m);
1187         String id = url.getLastPathSegment();
1188         return Integer.parseInt(id);
1189     }
1190 
1191     /**
1192      * Creates a new calendar, with the provided name, time zone, and account name, but an empty
1193      * owner account, which makes this calendar non-primary calendar.
1194      *
1195      * @return the new calendar's _ID value
1196      */
insertNonPrimaryCal(String name, String timezone, String account)1197     private int insertNonPrimaryCal(String name, String timezone, String account) {
1198         ContentValues m = new ContentValues();
1199         m.put(Calendars.NAME, name);
1200         m.put(Calendars.CALENDAR_DISPLAY_NAME, name);
1201         m.put(Calendars.CALENDAR_COLOR, 0xff123456);
1202         m.put(Calendars.CALENDAR_TIME_ZONE, timezone);
1203         m.put(Calendars.VISIBLE, 1);
1204         m.put(Calendars.CAL_SYNC1, CALENDAR_URL);
1205         m.put(Calendars.OWNER_ACCOUNT, "");
1206         m.put(Calendars.ACCOUNT_NAME,  account);
1207         m.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
1208         m.put(Calendars.SYNC_EVENTS,  1);
1209 
1210         Uri url = mResolver.insert(
1211                 addSyncQueryParams(mCalendarsUri, account, DEFAULT_ACCOUNT_TYPE), m);
1212         String id = url.getLastPathSegment();
1213         return Integer.parseInt(id);
1214     }
1215 
obsToString(Object... objs)1216     private String obsToString(Object... objs) {
1217         StringBuilder bob = new StringBuilder();
1218 
1219         for (Object obj : objs) {
1220             bob.append(obj.toString());
1221             bob.append('#');
1222         }
1223 
1224         return bob.toString();
1225     }
1226 
insertColor(long colorType, String colorKey, long color)1227     private Uri insertColor(long colorType, String colorKey, long color) {
1228         ContentValues m = new ContentValues();
1229         m.put(Colors.ACCOUNT_NAME, DEFAULT_ACCOUNT);
1230         m.put(Colors.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
1231         m.put(Colors.DATA, obsToString(colorType, colorKey, color));
1232         m.put(Colors.COLOR_TYPE, colorType);
1233         m.put(Colors.COLOR_KEY, colorKey);
1234         m.put(Colors.COLOR, color);
1235 
1236         Uri uri = CalendarContract.Colors.CONTENT_URI;
1237 
1238         return mResolver.insert(addSyncQueryParams(uri, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), m);
1239     }
1240 
updateAndCheckColor(long colorId, long colorType, String colorKey, long color)1241     private void updateAndCheckColor(long colorId, long colorType, String colorKey, long color) {
1242 
1243         Uri uri = CalendarContract.Colors.CONTENT_URI;
1244 
1245         final String where = Colors.ACCOUNT_NAME + "=? AND " + Colors.ACCOUNT_TYPE + "=? AND "
1246                 + Colors.COLOR_TYPE + "=? AND " + Colors.COLOR_KEY + "=?";
1247 
1248         String[] selectionArgs = new String[] {
1249                 DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, Long.toString(colorType), colorKey
1250         };
1251 
1252         ContentValues cv = new ContentValues();
1253         cv.put(Colors.COLOR, color);
1254         cv.put(Colors.DATA, obsToString(colorType, colorKey, color));
1255 
1256         int count = mResolver.update(
1257                 addSyncQueryParams(uri, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), cv, where,
1258                 selectionArgs);
1259 
1260         checkColor(colorId, colorType, colorKey, color);
1261 
1262         assertEquals(1, count);
1263     }
1264 
1265     /**
1266      * Constructs a URI from a base URI (e.g. "content://com.android.calendar/calendars"),
1267      * an account name, and an account type.
1268      */
addSyncQueryParams(Uri uri, String account, String accountType)1269     private Uri addSyncQueryParams(Uri uri, String account, String accountType) {
1270         return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
1271                 .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
1272                 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
1273     }
1274 
deleteMatchingCalendars(String selection, String[] selectionArgs)1275     private int deleteMatchingCalendars(String selection, String[] selectionArgs) {
1276         return mResolver.delete(mCalendarsUri, selection, selectionArgs);
1277     }
1278 
insertEvent(int calId, EventInfo event)1279     private Uri insertEvent(int calId, EventInfo event) {
1280         return insertEvent(calId, event, null);
1281     }
1282 
insertEvent(int calId, EventInfo event, ContentValues cv)1283     private Uri insertEvent(int calId, EventInfo event, ContentValues cv) {
1284         if (mWipe) {
1285             // Wipe instance table so it will be regenerated
1286             mMetaData.clearInstanceRange();
1287         }
1288 
1289         if (cv == null) {
1290             cv = eventInfoToContentValues(calId, event);
1291         }
1292 
1293         Uri url = mResolver.insert(mEventsUri, cv);
1294 
1295         // Create a fake _sync_id and add it to the event.  Update the database
1296         // directly so that we don't trigger any validation checks in the
1297         // CalendarProvider.
1298         long id = ContentUris.parseId(url);
1299         mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id);
1300         event.mSyncId = mGlobalSyncId;
1301         mGlobalSyncId += 1;
1302 
1303         return url;
1304     }
1305 
eventInfoToContentValues(int calId, EventInfo event)1306     private ContentValues eventInfoToContentValues(int calId, EventInfo event) {
1307         ContentValues m = new ContentValues();
1308         m.put(Events.CALENDAR_ID, calId);
1309         m.put(Events.TITLE, event.mTitle);
1310         m.put(Events.DTSTART, event.mDtstart);
1311         m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0);
1312 
1313         if (event.mRrule == null || mForceDtend) {
1314             // This is a normal event
1315             m.put(Events.DTEND, event.mDtend);
1316             m.remove(Events.DURATION);
1317         }
1318         if (event.mRrule != null) {
1319             // This is a repeating event
1320             m.put(Events.RRULE, event.mRrule);
1321             m.put(Events.DURATION, event.mDuration);
1322             m.remove(Events.DTEND);
1323         }
1324 
1325         if (event.mDescription != null) {
1326             m.put(Events.DESCRIPTION, event.mDescription);
1327         }
1328         if (event.mTimezone != null) {
1329             m.put(Events.EVENT_TIMEZONE, event.mTimezone);
1330         }
1331         if (event.mCustomAppPackage != null) {
1332             m.put(Events.CUSTOM_APP_PACKAGE, event.mCustomAppPackage);
1333         }
1334         if (event.mCustomAppUri != null) {
1335             m.put(Events.CUSTOM_APP_URI, event.mCustomAppUri);
1336         }
1337         if (event.mUid2445 != null) {
1338             m.put(Events.UID_2445, event.mUid2445);
1339         }
1340 
1341         if (event.mOriginalTitle != null) {
1342             // This is a recurrence exception.
1343             EventInfo recur = findEvent(event.mOriginalTitle);
1344             assertNotNull(recur);
1345             String syncId = String.format("%d", recur.mSyncId);
1346             m.put(Events.ORIGINAL_SYNC_ID, syncId);
1347             m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0);
1348             m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance);
1349         }
1350         return m;
1351     }
1352 
1353     /**
1354      * Deletes all the events that match the given title.
1355      * @param title the given title to match events on
1356      * @return the number of rows deleted
1357      */
deleteMatchingEvents(String title, String account, String accountType)1358     private int deleteMatchingEvents(String title, String account, String accountType) {
1359         Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID },
1360                 "title=?", new String[] { title }, null);
1361         int numRows = 0;
1362         while (cursor.moveToNext()) {
1363             long id = cursor.getLong(0);
1364             // Do delete as a sync adapter so event is really deleted, not just marked
1365             // as deleted.
1366             Uri uri = updatedUri(ContentUris.withAppendedId(Events.CONTENT_URI, id), true, account,
1367                     accountType);
1368             numRows += mResolver.delete(uri, null, null);
1369         }
1370         cursor.close();
1371         return numRows;
1372     }
1373 
1374     /**
1375      * Updates all the events that match the given title.
1376      * @param title the given title to match events on
1377      * @return the number of rows updated
1378      */
updateMatchingEvents(String title, ContentValues values)1379     private int updateMatchingEvents(String title, ContentValues values) {
1380         String[] projection = new String[] {
1381                 Events._ID,
1382                 Events.DTSTART,
1383                 Events.DTEND,
1384                 Events.DURATION,
1385                 Events.ALL_DAY,
1386                 Events.RRULE,
1387                 Events.EVENT_TIMEZONE,
1388                 Events.ORIGINAL_SYNC_ID,
1389         };
1390         Cursor cursor = mResolver.query(mEventsUri, projection,
1391                 "title=?", new String[] { title }, null);
1392         int numRows = 0;
1393         while (cursor.moveToNext()) {
1394             long id = cursor.getLong(0);
1395 
1396             // If any of the following fields are being changed, then we need
1397             // to include all of them.
1398             if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND)
1399                     || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY)
1400                     || values.containsKey(Events.RRULE)
1401                     || values.containsKey(Events.EVENT_TIMEZONE)
1402                     || values.containsKey(CalendarContract.Events.STATUS)) {
1403                 long dtstart = cursor.getLong(1);
1404                 long dtend = cursor.getLong(2);
1405                 String duration = cursor.getString(3);
1406                 boolean allDay = cursor.getInt(4) != 0;
1407                 String rrule = cursor.getString(5);
1408                 String timezone = cursor.getString(6);
1409                 String originalEvent = cursor.getString(7);
1410 
1411                 if (!values.containsKey(Events.DTSTART)) {
1412                     values.put(Events.DTSTART, dtstart);
1413                 }
1414                 // Don't add DTEND for repeating events
1415                 if (!values.containsKey(Events.DTEND) && rrule == null) {
1416                     values.put(Events.DTEND, dtend);
1417                 }
1418                 if (!values.containsKey(Events.DURATION) && duration != null) {
1419                     values.put(Events.DURATION, duration);
1420                 }
1421                 if (!values.containsKey(Events.ALL_DAY)) {
1422                     values.put(Events.ALL_DAY, allDay ? 1 : 0);
1423                 }
1424                 if (!values.containsKey(Events.RRULE) && rrule != null) {
1425                     values.put(Events.RRULE, rrule);
1426                 }
1427                 if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) {
1428                     values.put(Events.EVENT_TIMEZONE, timezone);
1429                 }
1430                 if (!values.containsKey(Events.ORIGINAL_SYNC_ID) && originalEvent != null) {
1431                     values.put(Events.ORIGINAL_SYNC_ID, originalEvent);
1432                 }
1433             }
1434 
1435             Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
1436             numRows += mResolver.update(uri, values, null, null);
1437         }
1438         cursor.close();
1439         return numRows;
1440     }
1441 
1442     /**
1443      * Updates the status of all the events that match the given title.
1444      * @param title the given title to match events on
1445      * @return the number of rows updated
1446      */
updateMatchingEventsStatusOnly(String title, ContentValues values)1447     private int updateMatchingEventsStatusOnly(String title, ContentValues values) {
1448         String[] projection = new String[] {
1449                 Events._ID,
1450         };
1451         if (values.size() != 1 && !values.containsKey(Events.STATUS)) {
1452             return 0;
1453         }
1454         Cursor cursor = mResolver.query(mEventsUri, projection,
1455                 "title=?", new String[] { title }, null);
1456         int numRows = 0;
1457         while (cursor.moveToNext()) {
1458             long id = cursor.getLong(0);
1459 
1460             Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
1461             numRows += mResolver.update(uri, values, null, null);
1462         }
1463         cursor.close();
1464         return numRows;
1465     }
1466 
1467 
deleteAllEvents()1468     private void deleteAllEvents() {
1469         mDb.execSQL("DELETE FROM Events;");
1470         mMetaData.clearInstanceRange();
1471     }
1472 
1473     /**
1474      * Creates an updated URI that includes query parameters that identify the source as a
1475      * sync adapter.
1476      */
asSyncAdapter(Uri uri, String account, String accountType)1477     static Uri asSyncAdapter(Uri uri, String account, String accountType) {
1478         return uri.buildUpon()
1479                 .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,
1480                         "true")
1481                 .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
1482                 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
1483     }
1484 
testInsertUpdateDeleteColor()1485     public void testInsertUpdateDeleteColor() throws Exception {
1486         // Calendar Color
1487         long colorType = Colors.TYPE_CALENDAR;
1488         String colorKey = "123";
1489         long colorValue = 11;
1490         long colorId = insertAndCheckColor(colorType, colorKey, colorValue);
1491 
1492         try {
1493             insertAndCheckColor(colorType, colorKey, colorValue);
1494             fail("Expected to fail with duplicate insertion");
1495         } catch (IllegalArgumentException iae) {
1496             // good
1497         }
1498 
1499         // Test Update
1500         colorValue += 11;
1501         updateAndCheckColor(colorId, colorType, colorKey, colorValue);
1502 
1503         // Event Color
1504         colorType = Colors.TYPE_EVENT;
1505         colorValue += 11;
1506         colorId = insertAndCheckColor(colorType, colorKey, colorValue);
1507         try {
1508             insertAndCheckColor(colorType, colorKey, colorValue);
1509             fail("Expected to fail with duplicate insertion");
1510         } catch (IllegalArgumentException iae) {
1511             // good
1512         }
1513 
1514         // Create an event with the old color value.
1515         int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
1516         String title = "colorTest";
1517         ContentValues cv = this.eventInfoToContentValues(calendarId0, mEvents[0]);
1518         cv.put(Events.EVENT_COLOR_KEY, colorKey);
1519         cv.put(Events.TITLE, title);
1520         Uri uri = insertEvent(calendarId0, mEvents[0], cv);
1521         Cursor c = mResolver.query(uri, new String[] {Events.EVENT_COLOR},  null, null, null);
1522         try {
1523             // Confirm the color is set.
1524             c.moveToFirst();
1525             assertEquals(colorValue, c.getInt(0));
1526         } finally {
1527             if (c != null) {
1528                 c.close();
1529             }
1530         }
1531 
1532         // Test Update
1533         colorValue += 11;
1534         updateAndCheckColor(colorId, colorType, colorKey, colorValue);
1535 
1536         // Check if color was updated in event.
1537         c = mResolver.query(uri, new String[] {Events.EVENT_COLOR}, null, null, null);
1538         try {
1539             c.moveToFirst();
1540             assertEquals(colorValue, c.getInt(0));
1541         } finally {
1542             if (c != null) {
1543                 c.close();
1544             }
1545         }
1546 
1547         // Test Delete
1548         Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, DEFAULT_ACCOUNT,
1549                 DEFAULT_ACCOUNT_TYPE);
1550         try {
1551             // Delete should fail if color referenced by an event.
1552             mResolver.delete(colSyncUri, WHERE_COLOR_ACCOUNT_AND_INDEX,
1553                     new String[] {DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, colorKey});
1554             fail("Should not allow deleting referenced color");
1555         } catch (UnsupportedOperationException e) {
1556             // Exception expected.
1557         }
1558         Cursor cursor = mResolver.query(Colors.CONTENT_URI, new String[] {Colors.COLOR_KEY},
1559                 Colors.COLOR_KEY + "=? AND " + Colors.COLOR_TYPE + "=?",
1560                 new String[] {colorKey, Long.toString(colorType)}, null);
1561         assertEquals(1, cursor.getCount());
1562 
1563         // Try again, by deleting the event, then the color.
1564         assertEquals(1, deleteMatchingEvents(title, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE));
1565         mResolver.delete(colSyncUri, WHERE_COLOR_ACCOUNT_AND_INDEX,
1566                 new String[] {DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, colorKey});
1567         cursor = mResolver.query(Colors.CONTENT_URI, new String[] {Colors.COLOR_KEY},
1568                 Colors.COLOR_KEY + "=? AND " + Colors.COLOR_TYPE + "=?",
1569                 new String[] {colorKey, Long.toString(colorType)}, null);
1570         assertEquals(0, cursor.getCount());
1571     }
1572 
checkColor(long colorId, long colorType, String colorKey, long color)1573     private void checkColor(long colorId, long colorType, String colorKey, long color) {
1574         String[] projection = new String[] {
1575                 Colors.ACCOUNT_NAME, // 0
1576                 Colors.ACCOUNT_TYPE, // 1
1577                 Colors.COLOR_TYPE,   // 2
1578                 Colors.COLOR_KEY,    // 3
1579                 Colors.COLOR,        // 4
1580                 Colors._ID,          // 5
1581                 Colors.DATA,         // 6
1582         };
1583         Cursor cursor = mResolver.query(Colors.CONTENT_URI, projection, Colors.COLOR_KEY
1584                 + "=? AND " + Colors.COLOR_TYPE + "=?", new String[] {
1585                 colorKey, Long.toString(colorType)
1586         }, null /* sortOrder */);
1587 
1588         assertEquals(1, cursor.getCount());
1589 
1590         assertTrue(cursor.moveToFirst());
1591         assertEquals(DEFAULT_ACCOUNT, cursor.getString(0));
1592         assertEquals(DEFAULT_ACCOUNT_TYPE, cursor.getString(1));
1593         assertEquals(colorType, cursor.getLong(2));
1594         assertEquals(colorKey, cursor.getString(3));
1595         assertEquals(color, cursor.getLong(4));
1596         assertEquals(colorId, cursor.getLong(5));
1597         assertEquals(obsToString(colorType, colorKey, color), cursor.getString(6));
1598         cursor.close();
1599     }
1600 
insertAndCheckColor(long colorType, String colorKey, long color)1601     private long insertAndCheckColor(long colorType, String colorKey, long color) {
1602         Uri uri = insertColor(colorType, colorKey, color);
1603         long id = Long.parseLong(uri.getLastPathSegment());
1604 
1605         checkColor(id, colorType, colorKey, color);
1606         return id;
1607     }
1608 
testInsertNormalEvents()1609     public void testInsertNormalEvents() throws Exception {
1610         final int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1611         Cursor cursor = mResolver.query(mEventsUri, null, null, null, null);
1612         assertEquals(0, cursor.getCount());
1613         cursor.close();
1614 
1615         // Keep track of the number of normal events
1616         int numOfInserts = 0;
1617 
1618         // "begin" is the earliest start time of all the normal events,
1619         // and "end" is the latest end time of all the normal events.
1620         long begin = 0, end = 0;
1621 
1622         int len = mEvents.length;
1623         Uri[] uris = new Uri[len];
1624         ContentValues[] cvs = new ContentValues[len];
1625         for (int ii = 0; ii < len; ii++) {
1626             EventInfo event = mEvents[ii];
1627             // Skip repeating events and recurrence exceptions
1628             if (event.mRrule != null || event.mOriginalTitle != null) {
1629                 continue;
1630             }
1631             if (numOfInserts == 0) {
1632                 begin = event.mDtstart;
1633                 end = event.mDtend;
1634             } else {
1635                 if (begin > event.mDtstart) {
1636                     begin = event.mDtstart;
1637                 }
1638                 if (end < event.mDtend) {
1639                     end = event.mDtend;
1640                 }
1641             }
1642 
1643             cvs[ii] = eventInfoToContentValues(calId, event);
1644             uris[ii] = insertEvent(calId, event, cvs[ii]);
1645             numOfInserts += 1;
1646         }
1647 
1648         // Verify
1649         for (int i = 0; i < len; i++) {
1650             if (cvs[i] == null) continue;
1651             assertNotNull(uris[i]);
1652             cursor = mResolver.query(uris[i], null, null, null, null);
1653             assertEquals("Item " + i + " not found", 1, cursor.getCount());
1654             verifyContentValueAgainstCursor(cvs[i], cvs[i].keySet(), cursor);
1655             cursor.close();
1656         }
1657 
1658         // query all
1659         cursor = mResolver.query(mEventsUri, null, null, null, null);
1660         assertEquals(numOfInserts, cursor.getCount());
1661         cursor.close();
1662 
1663         // Check that the Instances table has one instance of each of the
1664         // normal events.
1665         cursor = queryInstances(begin, end);
1666         assertEquals(numOfInserts, cursor.getCount());
1667         cursor.close();
1668     }
1669 
testInsertRepeatingEvents()1670     public void testInsertRepeatingEvents() throws Exception {
1671         Cursor cursor;
1672         Uri url = null;
1673 
1674         int calId = insertCal("Calendar0", "America/Los_Angeles");
1675 
1676         cursor = mResolver.query(mEventsUri, null, null, null, null);
1677         assertEquals(0, cursor.getCount());
1678         cursor.close();
1679 
1680         // Keep track of the number of repeating events
1681         int numOfInserts = 0;
1682 
1683         int len = mEvents.length;
1684         Uri[] uris = new Uri[len];
1685         ContentValues[] cvs = new ContentValues[len];
1686         for (int ii = 0; ii < len; ii++) {
1687             EventInfo event = mEvents[ii];
1688             // Skip normal events
1689             if (event.mRrule == null) {
1690                 continue;
1691             }
1692             cvs[ii] = eventInfoToContentValues(calId, event);
1693             uris[ii] = insertEvent(calId, event, cvs[ii]);
1694             numOfInserts += 1;
1695         }
1696 
1697         // Verify
1698         for (int i = 0; i < len; i++) {
1699             if (cvs[i] == null) continue;
1700             assertNotNull(uris[i]);
1701             cursor = mResolver.query(uris[i], null, null, null, null);
1702             assertEquals("Item " + i + " not found", 1, cursor.getCount());
1703             verifyContentValueAgainstCursor(cvs[i], cvs[i].keySet(), cursor);
1704             cursor.close();
1705         }
1706 
1707         // query all
1708         cursor = mResolver.query(mEventsUri, null, null, null, null);
1709         assertEquals(numOfInserts, cursor.getCount());
1710         cursor.close();
1711     }
1712 
1713     // Force a dtend value to be set and make sure instance expansion still works
testInstanceRangeDtend()1714     public void testInstanceRangeDtend() throws Exception {
1715         mForceDtend = true;
1716         testInstanceRange();
1717     }
1718 
testInstanceRange()1719     public void testInstanceRange() throws Exception {
1720         Cursor cursor;
1721         Uri url = null;
1722 
1723         int calId = insertCal("Calendar0", "America/Los_Angeles");
1724 
1725         cursor = mResolver.query(mEventsUri, null, null, null, null);
1726         assertEquals(0, cursor.getCount());
1727         cursor.close();
1728 
1729         int len = mInstanceRanges.length;
1730         for (int ii = 0; ii < len; ii++) {
1731             InstanceInfo instance = mInstanceRanges[ii];
1732             EventInfo event = instance.mEvent;
1733             url = insertEvent(calId, event);
1734             cursor = queryInstances(instance.mBegin, instance.mEnd);
1735             if (instance.mExpectedOccurrences != cursor.getCount()) {
1736                 Log.e(TAG, "Test failed! Instance index: " + ii);
1737                 Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription
1738                         + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]"
1739                         + " expected: " + instance.mExpectedOccurrences);
1740                 dumpCursor(cursor);
1741             }
1742             assertEquals(instance.mExpectedOccurrences, cursor.getCount());
1743             cursor.close();
1744             // Delete as sync_adapter so event is really deleted.
1745             int rows = mResolver.delete(
1746                     updatedUri(url, true, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
1747                     null /* selection */, null /* selection args */);
1748             assertEquals(1, rows);
1749         }
1750     }
1751 
assertArrayEquals(T[] expected, T[] actual)1752     public static <T> void assertArrayEquals(T[] expected, T[] actual) {
1753         if (!Arrays.equals(expected, actual)) {
1754             fail("expected:<" + Arrays.toString(expected) +
1755                 "> but was:<" + Arrays.toString(actual) + ">");
1756         }
1757     }
1758 
1759     @SmallTest @Smoke
testEscapeSearchToken()1760     public void testEscapeSearchToken() {
1761         String token = "test";
1762         String expected = "test";
1763         assertEquals(expected, mProvider.escapeSearchToken(token));
1764 
1765         token = "%";
1766         expected = "#%";
1767         assertEquals(expected, mProvider.escapeSearchToken(token));
1768 
1769         token = "_";
1770         expected = "#_";
1771         assertEquals(expected, mProvider.escapeSearchToken(token));
1772 
1773         token = "#";
1774         expected = "##";
1775         assertEquals(expected, mProvider.escapeSearchToken(token));
1776 
1777         token = "##";
1778         expected = "####";
1779         assertEquals(expected, mProvider.escapeSearchToken(token));
1780 
1781         token = "%_#";
1782         expected = "#%#_##";
1783         assertEquals(expected, mProvider.escapeSearchToken(token));
1784 
1785         token = "blah%blah";
1786         expected = "blah#%blah";
1787         assertEquals(expected, mProvider.escapeSearchToken(token));
1788     }
1789 
1790     @SmallTest @Smoke
testTokenizeSearchQuery()1791     public void testTokenizeSearchQuery() {
1792         String query = "";
1793         String[] expectedTokens = new String[] {};
1794         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1795 
1796         query = "a";
1797         expectedTokens = new String[] {"a"};
1798         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1799 
1800         query = "word";
1801         expectedTokens = new String[] {"word"};
1802         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1803 
1804         query = "two words";
1805         expectedTokens = new String[] {"two", "words"};
1806         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1807 
1808         query = "test, punctuation.";
1809         expectedTokens = new String[] {"test", "punctuation"};
1810         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1811 
1812         query = "\"test phrase\"";
1813         expectedTokens = new String[] {"test phrase"};
1814         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1815 
1816         query = "unquoted \"this is quoted\"";
1817         expectedTokens = new String[] {"unquoted", "this is quoted"};
1818         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1819 
1820         query = " \"this is quoted\"  unquoted ";
1821         expectedTokens = new String[] {"this is quoted", "unquoted"};
1822         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1823 
1824         query = "escap%e m_e";
1825         expectedTokens = new String[] {"escap#%e", "m#_e"};
1826         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1827 
1828         query = "'a bunch' of malformed\" things";
1829         expectedTokens = new String[] {"a", "bunch", "of", "malformed", "things"};
1830         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1831 
1832         query = "''''''....,.''trim punctuation";
1833         expectedTokens = new String[] {"trim", "punctuation"};
1834         assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1835     }
1836 
1837     @SmallTest @Smoke
testConstructSearchWhere()1838     public void testConstructSearchWhere() {
1839         String[] tokens = new String[] {"red"};
1840         String expected = "(title LIKE ? ESCAPE \"#\" OR "
1841             + "description LIKE ? ESCAPE \"#\" OR "
1842             + "eventLocation LIKE ? ESCAPE \"#\" OR "
1843             + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1844             + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1845         assertEquals(expected, mProvider.constructSearchWhere(tokens));
1846 
1847         tokens = new String[] {};
1848         expected = "";
1849         assertEquals(expected, mProvider.constructSearchWhere(tokens));
1850 
1851         tokens = new String[] {"red", "green"};
1852         expected = "(title LIKE ? ESCAPE \"#\" OR "
1853                 + "description LIKE ? ESCAPE \"#\" OR "
1854                 + "eventLocation LIKE ? ESCAPE \"#\" OR "
1855                 + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1856                 + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND "
1857                 + "(title LIKE ? ESCAPE \"#\" OR "
1858                 + "description LIKE ? ESCAPE \"#\" OR "
1859                 + "eventLocation LIKE ? ESCAPE \"#\" OR "
1860                 + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1861                 + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1862         assertEquals(expected, mProvider.constructSearchWhere(tokens));
1863 
1864         tokens = new String[] {"red blue", "green"};
1865         expected = "(title LIKE ? ESCAPE \"#\" OR "
1866             + "description LIKE ? ESCAPE \"#\" OR "
1867             + "eventLocation LIKE ? ESCAPE \"#\" OR "
1868             + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1869             + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND "
1870             + "(title LIKE ? ESCAPE \"#\" OR "
1871             + "description LIKE ? ESCAPE \"#\" OR "
1872             + "eventLocation LIKE ? ESCAPE \"#\" OR "
1873             + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1874             + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1875         assertEquals(expected, mProvider.constructSearchWhere(tokens));
1876     }
1877 
1878     @SmallTest @Smoke
testConstructSearchArgs()1879     public void testConstructSearchArgs() {
1880         String[] tokens = new String[] {"red"};
1881         String[] expected = new String[] {"%red%", "%red%",
1882                 "%red%", "%red%", "%red%" };
1883         assertArrayEquals(expected, mProvider.constructSearchArgs(tokens));
1884 
1885         tokens = new String[] {"red", "blue"};
1886         expected = new String[] { "%red%", "%red%", "%red%",
1887                 "%red%", "%red%", "%blue%", "%blue%",
1888                 "%blue%", "%blue%","%blue%"};
1889         assertArrayEquals(expected, mProvider.constructSearchArgs(tokens));
1890 
1891         tokens = new String[] {};
1892         expected = new String[] {};
1893         assertArrayEquals(expected, mProvider.constructSearchArgs(tokens));
1894     }
1895 
testInstanceSearchQuery()1896     public void testInstanceSearchQuery() throws Exception {
1897         final String[] PROJECTION = new String[] {
1898                 Instances.TITLE,                 // 0
1899                 Instances.EVENT_LOCATION,        // 1
1900                 Instances.ALL_DAY,               // 2
1901                 Instances.CALENDAR_COLOR,        // 3
1902                 Instances.EVENT_TIMEZONE,        // 4
1903                 Instances.EVENT_ID,              // 5
1904                 Instances.BEGIN,                 // 6
1905                 Instances.END,                   // 7
1906                 Instances._ID,                   // 8
1907                 Instances.START_DAY,             // 9
1908                 Instances.END_DAY,               // 10
1909                 Instances.START_MINUTE,          // 11
1910                 Instances.END_MINUTE,            // 12
1911                 Instances.HAS_ALARM,             // 13
1912                 Instances.RRULE,                 // 14
1913                 Instances.RDATE,                 // 15
1914                 Instances.SELF_ATTENDEE_STATUS,  // 16
1915                 Events.ORGANIZER,                // 17
1916                 Events.GUESTS_CAN_MODIFY,        // 18
1917         };
1918 
1919         String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW;
1920         String where = Instances.SELF_ATTENDEE_STATUS + "!=" +
1921                 CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED;
1922 
1923         int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1924         final String START = "2008-05-01T00:00:00";
1925         final String END = "2008-05-01T20:00:00";
1926 
1927         EventInfo event1 = new EventInfo("search orange",
1928                 START,
1929                 END,
1930                 false /* allDay */,
1931                 DEFAULT_TIMEZONE);
1932         event1.mDescription = "this is description1";
1933 
1934         EventInfo event2 = new EventInfo("search purple",
1935                 START,
1936                 END,
1937                 false /* allDay */,
1938                 DEFAULT_TIMEZONE);
1939         event2.mDescription = "lasers, out of nowhere";
1940 
1941         EventInfo event3 = new EventInfo("",
1942                 START,
1943                 END,
1944                 false /* allDay */,
1945                 DEFAULT_TIMEZONE);
1946         event3.mDescription = "kapow";
1947 
1948         EventInfo[] events = { event1, event2, event3 };
1949 
1950         insertEvent(calId, events[0]);
1951         insertEvent(calId, events[1]);
1952         insertEvent(calId, events[2]);
1953 
1954         Time time = new Time(DEFAULT_TIMEZONE);
1955         time.parse3339(START);
1956         long startMs = time.toMillis(true /* ignoreDst */);
1957         // Query starting from way in the past to one hour into the event.
1958         // Query is more than 2 months so the range won't get extended by the provider.
1959         Cursor cursor = null;
1960 
1961         try {
1962             cursor = queryInstances(mResolver, PROJECTION,
1963                     startMs - DateUtils.YEAR_IN_MILLIS,
1964                     startMs + DateUtils.HOUR_IN_MILLIS,
1965                     "search", where, null, orderBy);
1966             assertEquals(2, cursor.getCount());
1967         } finally {
1968             if (cursor != null) {
1969                 cursor.close();
1970             }
1971         }
1972 
1973         try {
1974             cursor = queryInstances(mResolver, PROJECTION,
1975                     startMs - DateUtils.YEAR_IN_MILLIS,
1976                     startMs + DateUtils.HOUR_IN_MILLIS,
1977                     "purple", where, null, orderBy);
1978             assertEquals(1, cursor.getCount());
1979         } finally {
1980             if (cursor != null) {
1981                 cursor.close();
1982             }
1983         }
1984 
1985         try {
1986             cursor = queryInstances(mResolver, PROJECTION,
1987                     startMs - DateUtils.YEAR_IN_MILLIS,
1988                     startMs + DateUtils.HOUR_IN_MILLIS,
1989                     "puurple", where, null, orderBy);
1990             assertEquals(0, cursor.getCount());
1991         } finally {
1992             if (cursor != null) {
1993                 cursor.close();
1994             }
1995         }
1996 
1997         try {
1998             cursor = queryInstances(mResolver, PROJECTION,
1999                     startMs - DateUtils.YEAR_IN_MILLIS,
2000                     startMs + DateUtils.HOUR_IN_MILLIS,
2001                     "purple lasers", where, null, orderBy);
2002             assertEquals(1, cursor.getCount());
2003         } finally {
2004             if (cursor != null) {
2005                 cursor.close();
2006             }
2007         }
2008 
2009         try {
2010             cursor = queryInstances(mResolver, PROJECTION,
2011                     startMs - DateUtils.YEAR_IN_MILLIS,
2012                     startMs + DateUtils.HOUR_IN_MILLIS,
2013                     "lasers kapow", where, null, orderBy);
2014             assertEquals(0, cursor.getCount());
2015         } finally {
2016             if (cursor != null) {
2017                 cursor.close();
2018             }
2019         }
2020 
2021         try {
2022             cursor = queryInstances(mResolver, PROJECTION,
2023                     startMs - DateUtils.YEAR_IN_MILLIS,
2024                     startMs + DateUtils.HOUR_IN_MILLIS,
2025                     "\"search purple\"", where, null, orderBy);
2026             assertEquals(1, cursor.getCount());
2027         } finally {
2028             if (cursor != null) {
2029                 cursor.close();
2030             }
2031         }
2032 
2033         try {
2034             cursor = queryInstances(mResolver, PROJECTION,
2035                     startMs - DateUtils.YEAR_IN_MILLIS,
2036                     startMs + DateUtils.HOUR_IN_MILLIS,
2037                     "\"purple search\"", where, null, orderBy);
2038             assertEquals(0, cursor.getCount());
2039         } finally {
2040             if (cursor != null) {
2041                 cursor.close();
2042             }
2043         }
2044 
2045         try {
2046             cursor = queryInstances(mResolver, PROJECTION,
2047                     startMs - DateUtils.YEAR_IN_MILLIS,
2048                     startMs + DateUtils.HOUR_IN_MILLIS,
2049                     "%", where, null, orderBy);
2050             assertEquals(0, cursor.getCount());
2051         } finally {
2052             if (cursor != null) {
2053                 cursor.close();
2054             }
2055         }
2056     }
2057 
testDeleteCalendar()2058     public void testDeleteCalendar() throws Exception {
2059         int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
2060         int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
2061         insertEvent(calendarId0, mEvents[0]);
2062         insertEvent(calendarId1, mEvents[1]);
2063         // Should have 2 calendars and 2 events
2064         testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 2);
2065         testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 2);
2066 
2067         int deletes = mResolver.delete(CalendarContract.Calendars.CONTENT_URI,
2068                 "ownerAccount='user2@google.com'", null /* selectionArgs */);
2069 
2070         assertEquals(1, deletes);
2071         // Should have 1 calendar and 1 event
2072         testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 1);
2073         testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 1);
2074 
2075         deletes = mResolver.delete(Uri.withAppendedPath(CalendarContract.Calendars.CONTENT_URI,
2076                 String.valueOf(calendarId0)),
2077                 null /* selection*/ , null /* selectionArgs */);
2078 
2079         assertEquals(1, deletes);
2080         // Should have 0 calendars and 0 events
2081         testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 0);
2082         testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 0);
2083 
2084         deletes = mResolver.delete(CalendarContract.Calendars.CONTENT_URI,
2085                 "ownerAccount=?", new String[] {"user2@google.com"} /* selectionArgs */);
2086 
2087         assertEquals(0, deletes);
2088     }
2089 
testCalendarAlerts()2090     public void testCalendarAlerts() throws Exception {
2091         // This projection is from AlertActivity; want to make sure it works.
2092         String[] projection = new String[] {
2093                 CalendarContract.CalendarAlerts._ID,              // 0
2094                 CalendarContract.CalendarAlerts.TITLE,            // 1
2095                 CalendarContract.CalendarAlerts.EVENT_LOCATION,   // 2
2096                 CalendarContract.CalendarAlerts.ALL_DAY,          // 3
2097                 CalendarContract.CalendarAlerts.BEGIN,            // 4
2098                 CalendarContract.CalendarAlerts.END,              // 5
2099                 CalendarContract.CalendarAlerts.EVENT_ID,         // 6
2100                 CalendarContract.CalendarAlerts.CALENDAR_COLOR,   // 7
2101                 CalendarContract.CalendarAlerts.RRULE,            // 8
2102                 CalendarContract.CalendarAlerts.HAS_ALARM,        // 9
2103                 CalendarContract.CalendarAlerts.STATE,            // 10
2104                 CalendarContract.CalendarAlerts.ALARM_TIME,       // 11
2105         };
2106 
2107         mCalendarId = insertCal("CalendarTestAttendees", DEFAULT_TIMEZONE);
2108         String calendarIdString = Integer.toString(mCalendarId);
2109         checkEvents(0, mDb, calendarIdString);
2110         Uri eventUri = insertEvent(mCalendarId, findEvent("normal0"));
2111         checkEvents(1, mDb, calendarIdString);
2112         long eventId = ContentUris.parseId(eventUri);
2113 
2114         Uri alertUri = CalendarContract.CalendarAlerts.insert(mResolver, eventId /* eventId */,
2115                 2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */);
2116         CalendarContract.CalendarAlerts.insert(mResolver, eventId /* eventId */,
2117                 2 /* begin */, 7 /* end */, 8 /* alarmTime */, 9 /* minutes */);
2118 
2119         // Regular query
2120         Cursor cursor = mResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI, projection,
2121                 null /* selection */, null /* selectionArgs */, null /* sortOrder */);
2122 
2123         assertEquals(2, cursor.getCount());
2124         cursor.close();
2125 
2126         // Instance query
2127         cursor = mResolver.query(alertUri, projection,
2128                 null /* selection */, null /* selectionArgs */, null /* sortOrder */);
2129 
2130         assertEquals(1, cursor.getCount());
2131         cursor.close();
2132 
2133         // Grouped by event query
2134         cursor = mResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI_BY_INSTANCE,
2135                 projection, null /* selection */, null /* selectionArgs */, null /* sortOrder */);
2136 
2137         assertEquals(1, cursor.getCount());
2138         cursor.close();
2139     }
2140 
testInsertAlertToNonExistentEvent()2141     public void testInsertAlertToNonExistentEvent() {
2142         Uri alertUri = CalendarContract.CalendarAlerts.insert(mResolver, 1 /* eventId */,
2143                 2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */);
2144         assertEquals(null, alertUri);
2145     }
2146 
testInsertReminderToNonExistentEvent()2147     public void testInsertReminderToNonExistentEvent() {
2148         ContentValues reminder = new ContentValues();
2149         reminder.put(CalendarContract.Reminders.MINUTES, 30);
2150         reminder.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_EMAIL);
2151         reminder.put(CalendarContract.Attendees.EVENT_ID, 1);
2152         Uri reminderUri = mResolver.insert(
2153                 updatedUri(CalendarContract.Reminders.CONTENT_URI, true, DEFAULT_ACCOUNT,
2154                         DEFAULT_ACCOUNT_TYPE), reminder);
2155         assertEquals(null, reminderUri);
2156     }
2157 
testInsertAttendeeToNonExistentEvent()2158     public void testInsertAttendeeToNonExistentEvent() {
2159         ContentValues attendee = new ContentValues();
2160         attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe");
2161         attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT);
2162         attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE,
2163                 CalendarContract.Attendees.TYPE_REQUIRED);
2164         attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,
2165                 CalendarContract.Attendees.RELATIONSHIP_ORGANIZER);
2166         attendee.put(CalendarContract.Attendees.EVENT_ID, 1);
2167         attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID1");
2168         attendee.put(CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE, "IDNS1");
2169         Uri attendeesUri = mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee);
2170         assertEquals(null, attendeesUri);
2171     }
2172 
testInsertExtendedPropertyToNonExistentEvent()2173     public void testInsertExtendedPropertyToNonExistentEvent() {
2174         ContentValues extended = new ContentValues();
2175         extended.put(CalendarContract.ExtendedProperties.NAME, "foo");
2176         extended.put(CalendarContract.ExtendedProperties.VALUE, "bar");
2177         extended.put(CalendarContract.ExtendedProperties.EVENT_ID, 1);
2178 
2179         Uri extendedUri = mResolver.insert(
2180                 updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, true,
2181                         DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended);
2182         assertEquals(null, extendedUri);
2183     }
2184 
checkEvents(int count, SQLiteDatabase db)2185     void checkEvents(int count, SQLiteDatabase db) {
2186         Cursor cursor = db.query("Events", null, null, null, null, null, null);
2187         try {
2188             assertEquals(count, cursor.getCount());
2189         } finally {
2190             cursor.close();
2191         }
2192     }
2193 
checkEvents(int count, SQLiteDatabase db, String calendar)2194     void checkEvents(int count, SQLiteDatabase db, String calendar) {
2195         Cursor cursor = db.query("Events", null, Events.CALENDAR_ID + "=?", new String[] {calendar},
2196                 null, null, null);
2197         try {
2198             assertEquals(count, cursor.getCount());
2199         } finally {
2200             cursor.close();
2201         }
2202     }
2203 
2204 
2205 //    TODO Reenable this when we are ready to work on this
2206 //
2207 //    public void testToShowInsertIsSlowForRecurringEvents() throws Exception {
2208 //        mCalendarId = insertCal("CalendarTestToShowInsertIsSlowForRecurringEvents", DEFAULT_TIMEZONE);
2209 //        String calendarIdString = Integer.toString(mCalendarId);
2210 //        long testStart = System.currentTimeMillis();
2211 //
2212 //        final int testTrials = 100;
2213 //
2214 //        for (int i = 0; i < testTrials; i++) {
2215 //            checkEvents(i, mDb, calendarIdString);
2216 //            long insertStartTime = System.currentTimeMillis();
2217 //            Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
2218 //            Log.e(TAG, i + ") insertion time " + (System.currentTimeMillis() - insertStartTime));
2219 //        }
2220 //        Log.e(TAG, " Avg insertion time = " + (System.currentTimeMillis() - testStart)/testTrials);
2221 //    }
2222 
2223     /**
2224      * Test attendee processing
2225      * @throws Exception
2226      */
testAttendees()2227     public void testAttendees() throws Exception {
2228         mCalendarId = insertCal("CalendarTestAttendees", DEFAULT_TIMEZONE);
2229         String calendarIdString = Integer.toString(mCalendarId);
2230         checkEvents(0, mDb, calendarIdString);
2231         Uri eventUri = insertEvent(mCalendarId, findEvent("normal0"));
2232         checkEvents(1, mDb, calendarIdString);
2233         long eventId = ContentUris.parseId(eventUri);
2234 
2235         ContentValues attendee = new ContentValues();
2236         attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe");
2237         attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT);
2238         attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE,
2239                 CalendarContract.Attendees.TYPE_REQUIRED);
2240         attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,
2241                 CalendarContract.Attendees.RELATIONSHIP_ORGANIZER);
2242         attendee.put(CalendarContract.Attendees.EVENT_ID, eventId);
2243         attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID1");
2244         attendee.put(CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE, "IDNS1");
2245         Uri attendeesUri = mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee);
2246 
2247         Cursor cursor = mResolver.query(CalendarContract.Attendees.CONTENT_URI, null,
2248                 "event_id=" + eventId, null, null);
2249         assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1,
2250                 cursor.getCount());
2251         Set<String> attendeeColumns = attendee.keySet();
2252         verifyContentValueAgainstCursor(attendee, attendeeColumns, cursor);
2253         cursor.close();
2254 
2255         cursor = mResolver.query(eventUri, null, null, null, null);
2256         // TODO figure out why this test fails. App works fine for this case.
2257         assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1,
2258                 cursor.getCount());
2259         int selfColumn = cursor.getColumnIndex(CalendarContract.Events.SELF_ATTENDEE_STATUS);
2260         cursor.moveToNext();
2261         long selfAttendeeStatus = cursor.getInt(selfColumn);
2262         assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus);
2263         cursor.close();
2264 
2265         // Update status to declined and change identity
2266         ContentValues attendeeUpdate = new ContentValues();
2267         attendeeUpdate.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID2");
2268         attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID2");
2269         attendeeUpdate.put(CalendarContract.Attendees.ATTENDEE_STATUS,
2270                 CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED);
2271         attendee.put(CalendarContract.Attendees.ATTENDEE_STATUS,
2272                 CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED);
2273         mResolver.update(attendeesUri, attendeeUpdate, null, null);
2274 
2275         // Check in attendees table
2276         cursor = mResolver.query(attendeesUri, null, null, null, null);
2277         cursor.moveToNext();
2278         verifyContentValueAgainstCursor(attendee, attendeeColumns, cursor);
2279         cursor.close();
2280 
2281         // Test that the self status in events table is updated
2282         cursor = mResolver.query(eventUri, null, null, null, null);
2283         cursor.moveToNext();
2284         selfAttendeeStatus = cursor.getInt(selfColumn);
2285         assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
2286         cursor.close();
2287 
2288         // Add another attendee
2289         attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Dude");
2290         attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, "dude@dude.com");
2291         attendee.put(CalendarContract.Attendees.ATTENDEE_STATUS,
2292                 CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED);
2293         mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee);
2294 
2295         cursor = mResolver.query(CalendarContract.Attendees.CONTENT_URI, null,
2296                 "event_id=" + eventId, null, null);
2297         assertEquals(2, cursor.getCount());
2298         cursor.close();
2299 
2300         cursor = mResolver.query(eventUri, null, null, null, null);
2301         cursor.moveToNext();
2302         selfAttendeeStatus = cursor.getInt(selfColumn);
2303         assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
2304         cursor.close();
2305     }
2306 
verifyContentValueAgainstCursor(ContentValues cv, Set<String> keys, Cursor cursor)2307     private void verifyContentValueAgainstCursor(ContentValues cv,
2308             Set<String> keys, Cursor cursor) {
2309         cursor.moveToFirst();
2310         for (String key : keys) {
2311             assertEquals(cv.get(key).toString(),
2312                     cursor.getString(cursor.getColumnIndex(key)));
2313         }
2314         cursor.close();
2315     }
2316 
2317     /**
2318      * Test the event's dirty status and clear it.
2319      *
2320      * @param eventId event to fetch.
2321      * @param wanted the wanted dirty status
2322      */
testAndClearDirty(long eventId, int wanted)2323     private void testAndClearDirty(long eventId, int wanted) {
2324         Cursor cursor = mResolver.query(
2325                 ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId),
2326                 null, null, null, null);
2327         try {
2328             assertEquals("Event count", 1, cursor.getCount());
2329             cursor.moveToNext();
2330             int dirty = cursor.getInt(cursor.getColumnIndex(CalendarContract.Events.DIRTY));
2331             assertEquals("dirty flag", wanted, dirty);
2332             if (dirty == 1) {
2333                 // Have to access database directly since provider will set dirty again.
2334                 mDb.execSQL("UPDATE Events SET " + Events.DIRTY + "=0 WHERE _id=" + eventId);
2335             }
2336         } finally {
2337             cursor.close();
2338         }
2339     }
2340 
2341     /**
2342      * Test the count of results from a query.
2343      * @param uri The URI to query
2344      * @param where The where string or null.
2345      * @param wanted The number of results wanted.  An assertion is thrown if it doesn't match.
2346      */
testQueryCount(Uri uri, String where, int wanted)2347     private void testQueryCount(Uri uri, String where, int wanted) {
2348         Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */,
2349                 null /* sortOrder */);
2350         try {
2351             assertEquals("query results", wanted, cursor.getCount());
2352         } finally {
2353             cursor.close();
2354         }
2355     }
2356 
2357     /**
2358      * Test dirty flag processing.
2359      * @throws Exception
2360      */
testDirty()2361     public void testDirty() throws Exception {
2362         internalTestDirty(false);
2363     }
2364 
2365     /**
2366      * Test dirty flag processing for updates from a sync adapter.
2367      * @throws Exception
2368      */
testDirtyWithSyncAdapter()2369     public void testDirtyWithSyncAdapter() throws Exception {
2370         internalTestDirty(true);
2371     }
2372 
2373     /**
2374      * Adds CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation.  Otherwise,
2375      * returns the original URI.
2376      */
updatedUri(Uri uri, boolean syncAdapter, String account, String accountType)2377     private Uri updatedUri(Uri uri, boolean syncAdapter, String account, String accountType) {
2378         if (syncAdapter) {
2379             return addSyncQueryParams(uri, account, accountType);
2380         } else {
2381             return uri;
2382         }
2383     }
2384 
2385     /**
2386      * Test dirty flag processing either for syncAdapter operations or client operations.
2387      * The main difference is syncAdapter operations don't set the dirty bit.
2388      */
internalTestDirty(boolean syncAdapter)2389     private void internalTestDirty(boolean syncAdapter) throws Exception {
2390         mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2391 
2392         long now = System.currentTimeMillis();
2393         long begin = (now / 1000) * 1000;
2394         long end = begin + ONE_HOUR_MILLIS;
2395         Time time = new Time(DEFAULT_TIMEZONE);
2396         time.set(begin);
2397         String startDate = time.format3339(false);
2398         time.set(end);
2399         String endDate = time.format3339(false);
2400 
2401         EventInfo eventInfo = new EventInfo("current", startDate, endDate, false);
2402         Uri eventUri = insertEvent(mCalendarId, eventInfo);
2403 
2404         long eventId = ContentUris.parseId(eventUri);
2405         testAndClearDirty(eventId, 1);
2406 
2407         ContentValues attendee = new ContentValues();
2408         attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe");
2409         attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT);
2410         attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE,
2411                 CalendarContract.Attendees.TYPE_REQUIRED);
2412         attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,
2413                 CalendarContract.Attendees.RELATIONSHIP_ORGANIZER);
2414         attendee.put(CalendarContract.Attendees.EVENT_ID, eventId);
2415 
2416         Uri attendeeUri = mResolver.insert(
2417                 updatedUri(CalendarContract.Attendees.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT,
2418                         DEFAULT_ACCOUNT_TYPE),
2419                 attendee);
2420         testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2421         testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
2422 
2423         ContentValues reminder = new ContentValues();
2424         reminder.put(CalendarContract.Reminders.MINUTES, 30);
2425         reminder.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_EMAIL);
2426         reminder.put(CalendarContract.Attendees.EVENT_ID, eventId);
2427 
2428         Uri reminderUri = mResolver.insert(
2429                 updatedUri(CalendarContract.Reminders.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT,
2430                         DEFAULT_ACCOUNT_TYPE), reminder);
2431         testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2432         testQueryCount(CalendarContract.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
2433 
2434         long alarmTime = begin + 5 * ONE_MINUTE_MILLIS;
2435 
2436         ContentValues alert = new ContentValues();
2437         alert.put(CalendarContract.CalendarAlerts.BEGIN, begin);
2438         alert.put(CalendarContract.CalendarAlerts.END, end);
2439         alert.put(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime);
2440         alert.put(CalendarContract.CalendarAlerts.CREATION_TIME, now);
2441         alert.put(CalendarContract.CalendarAlerts.RECEIVED_TIME, now);
2442         alert.put(CalendarContract.CalendarAlerts.NOTIFY_TIME, now);
2443         alert.put(CalendarContract.CalendarAlerts.STATE,
2444                 CalendarContract.CalendarAlerts.STATE_SCHEDULED);
2445         alert.put(CalendarContract.CalendarAlerts.MINUTES, 30);
2446         alert.put(CalendarContract.CalendarAlerts.EVENT_ID, eventId);
2447 
2448         Uri alertUri = mResolver.insert(
2449                 updatedUri(CalendarContract.CalendarAlerts.CONTENT_URI, syncAdapter,
2450                         DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert);
2451         // Alerts don't dirty the event
2452         testAndClearDirty(eventId, 0);
2453         testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
2454 
2455         ContentValues extended = new ContentValues();
2456         extended.put(CalendarContract.ExtendedProperties.NAME, "foo");
2457         extended.put(CalendarContract.ExtendedProperties.VALUE, "bar");
2458         extended.put(CalendarContract.ExtendedProperties.EVENT_ID, eventId);
2459 
2460         Uri extendedUri = null;
2461         if (syncAdapter) {
2462             // Only the sync adapter is allowed to modify ExtendedProperties.
2463             extendedUri = mResolver.insert(
2464                     updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, syncAdapter,
2465                             DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended);
2466             testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2467             testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI,
2468                     "event_id=" + eventId, 1);
2469         } else {
2470             // Confirm that inserting as app fails.
2471             try {
2472                 extendedUri = mResolver.insert(
2473                         updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, syncAdapter,
2474                                 DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended);
2475                 fail("Only sync adapter should be allowed to insert into ExtendedProperties");
2476             } catch (IllegalArgumentException iae) {}
2477         }
2478 
2479         // Now test updates
2480 
2481         attendee = new ContentValues();
2482         attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Sam");
2483 
2484         assertEquals("update", 1, mResolver.update(
2485                 updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2486                 attendee,
2487                 null /* where */, null /* selectionArgs */));
2488         testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2489 
2490         testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
2491 
2492         alert = new ContentValues();
2493         alert.put(CalendarContract.CalendarAlerts.STATE,
2494                 CalendarContract.CalendarAlerts.STATE_DISMISSED);
2495 
2496         assertEquals("update", 1, mResolver.update(
2497                 updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert,
2498                 null /* where */, null /* selectionArgs */));
2499         // Alerts don't dirty the event
2500         testAndClearDirty(eventId, 0);
2501         testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
2502 
2503         extended = new ContentValues();
2504         extended.put(CalendarContract.ExtendedProperties.VALUE, "baz");
2505 
2506         if (syncAdapter) {
2507             assertEquals("update", 1, mResolver.update(
2508                     updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2509                     extended,
2510                     null /* where */, null /* selectionArgs */));
2511             testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2512             testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI,
2513                     "event_id=" + eventId, 1);
2514         }
2515 
2516         // Now test deletes
2517 
2518         assertEquals("delete", 1, mResolver.delete(
2519                 updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2520                 null, null /* selectionArgs */));
2521         testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2522         testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 0);
2523 
2524         assertEquals("delete", 1, mResolver.delete(
2525                 updatedUri(reminderUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2526                 null /* where */, null /* selectionArgs */));
2527 
2528         testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2529         testQueryCount(CalendarContract.Reminders.CONTENT_URI, "event_id=" + eventId, 0);
2530 
2531         assertEquals("delete", 1, mResolver.delete(
2532                 updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2533                 null /* where */, null /* selectionArgs */));
2534 
2535         // Alerts don't dirty the event
2536         testAndClearDirty(eventId, 0);
2537         testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0);
2538 
2539         if (syncAdapter) {
2540             assertEquals("delete", 1, mResolver.delete(
2541                     updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2542                     null /* where */, null /* selectionArgs */));
2543 
2544             testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2545             testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI, "event_id=" + eventId,
2546                     0);
2547         }
2548     }
2549 
2550     /**
2551      * Test calendar deletion
2552      * @throws Exception
2553      */
testCalendarDeletion()2554     public void testCalendarDeletion() throws Exception {
2555         mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2556         Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
2557         long eventId = ContentUris.parseId(eventUri);
2558         testAndClearDirty(eventId, 1);
2559         Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1"));
2560         long eventId1 = ContentUris.parseId(eventUri);
2561         assertEquals("delete", 1, mResolver.delete(eventUri1, null, null));
2562         // Calendar has one event and one deleted event
2563         testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2);
2564 
2565         assertEquals("delete", 1, mResolver.delete(CalendarContract.Calendars.CONTENT_URI,
2566                 "_id=" + mCalendarId, null));
2567         // Calendar should be deleted
2568         testQueryCount(CalendarContract.Calendars.CONTENT_URI, null, 0);
2569         // Event should be gone
2570         testQueryCount(CalendarContract.Events.CONTENT_URI, null, 0);
2571     }
2572 
2573     /**
2574      * Test multiple account support.
2575      */
testMultipleAccounts()2576     public void testMultipleAccounts() throws Exception {
2577         mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2578         int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
2579         Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0"));
2580         Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1"));
2581 
2582         testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2);
2583         Uri eventsWithAccount = CalendarContract.Events.CONTENT_URI.buildUpon()
2584                 .appendQueryParameter(CalendarContract.EventsEntity.ACCOUNT_NAME, DEFAULT_ACCOUNT)
2585                 .appendQueryParameter(CalendarContract.EventsEntity.ACCOUNT_TYPE,
2586                         DEFAULT_ACCOUNT_TYPE)
2587                 .build();
2588         // Only one event for that account
2589         testQueryCount(eventsWithAccount, null, 1);
2590 
2591         // Test deletion with account and selection
2592 
2593         long eventId = ContentUris.parseId(eventUri1);
2594         // Wrong account, should not be deleted
2595         assertEquals("delete", 0, mResolver.delete(
2596                 updatedUri(eventsWithAccount, true /* syncAdapter */, DEFAULT_ACCOUNT,
2597                         DEFAULT_ACCOUNT_TYPE),
2598                 "_id=" + eventId, null /* selectionArgs */));
2599         testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2);
2600         // Right account, should be deleted
2601         assertEquals("delete", 1, mResolver.delete(
2602                 updatedUri(CalendarContract.Events.CONTENT_URI, true /* syncAdapter */,
2603                         "user2@google.com", DEFAULT_ACCOUNT_TYPE),
2604                 "_id=" + eventId, null /* selectionArgs */));
2605         testQueryCount(CalendarContract.Events.CONTENT_URI, null, 1);
2606     }
2607 
2608     /**
2609      * Run commands, wiping instance table at each step.
2610      * This tests full instance expansion.
2611      * @throws Exception
2612      */
testCommandSequences1()2613     public void testCommandSequences1() throws Exception {
2614         commandSequences(true);
2615     }
2616 
2617     /**
2618      * Run commands normally.
2619      * This tests incremental instance expansion.
2620      * @throws Exception
2621      */
testCommandSequences2()2622     public void testCommandSequences2() throws Exception {
2623         commandSequences(false);
2624     }
2625 
2626     /**
2627      * Run thorough set of command sequences
2628      * @param wipe true if instances should be wiped and regenerated
2629      * @throws Exception
2630      */
commandSequences(boolean wipe)2631     private void commandSequences(boolean wipe) throws Exception {
2632         Cursor cursor;
2633         Uri url = null;
2634         mWipe = wipe; // Set global flag
2635 
2636         mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2637 
2638         cursor = mResolver.query(mEventsUri, null, null, null, null);
2639         assertEquals(0, cursor.getCount());
2640         cursor.close();
2641         Command[] commands;
2642 
2643         Log.i(TAG, "Normal insert/delete");
2644         commands = mNormalInsertDelete;
2645         for (Command command : commands) {
2646             command.execute();
2647         }
2648 
2649         deleteAllEvents();
2650 
2651         Log.i(TAG, "All-day insert/delete");
2652         commands = mAlldayInsertDelete;
2653         for (Command command : commands) {
2654             command.execute();
2655         }
2656 
2657         deleteAllEvents();
2658 
2659         Log.i(TAG, "Recurring insert/delete");
2660         commands = mRecurringInsertDelete;
2661         for (Command command : commands) {
2662             command.execute();
2663         }
2664 
2665         deleteAllEvents();
2666 
2667         Log.i(TAG, "Exception with truncated recurrence");
2668         commands = mExceptionWithTruncatedRecurrence;
2669         for (Command command : commands) {
2670             command.execute();
2671         }
2672 
2673         deleteAllEvents();
2674 
2675         Log.i(TAG, "Exception with moved recurrence");
2676         commands = mExceptionWithMovedRecurrence;
2677         for (Command command : commands) {
2678             command.execute();
2679         }
2680 
2681         deleteAllEvents();
2682 
2683         Log.i(TAG, "Exception with cancel");
2684         commands = mCancelInstance;
2685         for (Command command : commands) {
2686             command.execute();
2687         }
2688 
2689         deleteAllEvents();
2690 
2691         Log.i(TAG, "Exception with moved recurrence2");
2692         commands = mExceptionWithMovedRecurrence2;
2693         for (Command command : commands) {
2694             command.execute();
2695         }
2696 
2697         deleteAllEvents();
2698 
2699         Log.i(TAG, "Exception with no recurrence");
2700         commands = mExceptionWithNoRecurrence;
2701         for (Command command : commands) {
2702             command.execute();
2703         }
2704     }
2705 
2706     /**
2707      * Test Time toString.
2708      * @throws Exception
2709      */
2710     // Suppressed because toString currently hangs.
2711     @Suppress
testTimeToString()2712     public void testTimeToString() throws Exception {
2713         Time time = new Time(Time.TIMEZONE_UTC);
2714         String str = "2039-01-01T23:00:00.000Z";
2715         String result = "20390101T230000UTC(0,0,0,-1,0)";
2716         time.parse3339(str);
2717         assertEquals(result, time.toString());
2718     }
2719 
2720     /**
2721      * Test the query done by Event.loadEvents
2722      * Also test that instance queries work when an event straddles the expansion range
2723      * @throws Exception
2724      */
testInstanceQuery()2725     public void testInstanceQuery() throws Exception {
2726         final String[] PROJECTION = new String[] {
2727                 Instances.TITLE,                 // 0
2728                 Instances.EVENT_LOCATION,        // 1
2729                 Instances.ALL_DAY,               // 2
2730                 Instances.CALENDAR_COLOR,        // 3
2731                 Instances.EVENT_TIMEZONE,        // 4
2732                 Instances.EVENT_ID,              // 5
2733                 Instances.BEGIN,                 // 6
2734                 Instances.END,                   // 7
2735                 Instances._ID,                   // 8
2736                 Instances.START_DAY,             // 9
2737                 Instances.END_DAY,               // 10
2738                 Instances.START_MINUTE,          // 11
2739                 Instances.END_MINUTE,            // 12
2740                 Instances.HAS_ALARM,             // 13
2741                 Instances.RRULE,                 // 14
2742                 Instances.RDATE,                 // 15
2743                 Instances.SELF_ATTENDEE_STATUS,  // 16
2744                 Events.ORGANIZER,                // 17
2745                 Events.GUESTS_CAN_MODIFY,        // 18
2746         };
2747 
2748         String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW;
2749         String where = Instances.SELF_ATTENDEE_STATUS + "!="
2750                 + CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED;
2751 
2752         int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2753         final String START = "2008-05-01T00:00:00";
2754         final String END = "2008-05-01T20:00:00";
2755 
2756         EventInfo[] events = { new EventInfo("normal0",
2757                 START,
2758                 END,
2759                 false /* allDay */,
2760                 DEFAULT_TIMEZONE) };
2761 
2762         insertEvent(calId, events[0]);
2763 
2764         Time time = new Time(DEFAULT_TIMEZONE);
2765         time.parse3339(START);
2766         long startMs = time.toMillis(true /* ignoreDst */);
2767         // Query starting from way in the past to one hour into the event.
2768         // Query is more than 2 months so the range won't get extended by the provider.
2769         Cursor cursor = queryInstances(mResolver, PROJECTION,
2770                 startMs - DateUtils.YEAR_IN_MILLIS, startMs + DateUtils.HOUR_IN_MILLIS,
2771                 where, null, orderBy);
2772         try {
2773             assertEquals(1, cursor.getCount());
2774         } finally {
2775             cursor.close();
2776         }
2777 
2778         // Now expand the instance range.  The event overlaps the new part of the range.
2779         cursor = queryInstances(mResolver, PROJECTION,
2780                 startMs - DateUtils.YEAR_IN_MILLIS, startMs + 2 * DateUtils.HOUR_IN_MILLIS,
2781                 where, null, orderBy);
2782         try {
2783             assertEquals(1, cursor.getCount());
2784         } finally {
2785             cursor.close();
2786         }
2787     }
2788 
2789     /**
2790      * Performs a query to return all visible instances in the given range that
2791      * match the given selection. This is a blocking function and should not be
2792      * done on the UI thread. This will cause an expansion of recurring events
2793      * to fill this time range if they are not already expanded and will slow
2794      * down for larger time ranges with many recurring events.
2795      *
2796      * @param cr The ContentResolver to use for the query
2797      * @param projection The columns to return
2798      * @param begin The start of the time range to query in UTC millis since
2799      *            epoch
2800      * @param end The end of the time range to query in UTC millis since epoch
2801      * @param selection Filter on the query as an SQL WHERE statement
2802      * @param selectionArgs Args to replace any '?'s in the selection
2803      * @param orderBy How to order the rows as an SQL ORDER BY statement
2804      * @return A Cursor of instances matching the selection
2805      */
queryInstances(ContentResolver cr, String[] projection, long begin, long end, String selection, String[] selectionArgs, String orderBy)2806     private static final Cursor queryInstances(ContentResolver cr, String[] projection, long begin,
2807             long end, String selection, String[] selectionArgs, String orderBy) {
2808 
2809         Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
2810         ContentUris.appendId(builder, begin);
2811         ContentUris.appendId(builder, end);
2812         if (TextUtils.isEmpty(selection)) {
2813             selection = WHERE_CALENDARS_SELECTED;
2814             selectionArgs = WHERE_CALENDARS_ARGS;
2815         } else {
2816             selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED;
2817             if (selectionArgs != null && selectionArgs.length > 0) {
2818                 selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1);
2819                 selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0];
2820             } else {
2821                 selectionArgs = WHERE_CALENDARS_ARGS;
2822             }
2823         }
2824         return cr.query(builder.build(), projection, selection, selectionArgs,
2825                 orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
2826     }
2827 
2828     /**
2829      * Performs a query to return all visible instances in the given range that
2830      * match the given selection. This is a blocking function and should not be
2831      * done on the UI thread. This will cause an expansion of recurring events
2832      * to fill this time range if they are not already expanded and will slow
2833      * down for larger time ranges with many recurring events.
2834      *
2835      * @param cr The ContentResolver to use for the query
2836      * @param projection The columns to return
2837      * @param begin The start of the time range to query in UTC millis since
2838      *            epoch
2839      * @param end The end of the time range to query in UTC millis since epoch
2840      * @param searchQuery A string of space separated search terms. Segments
2841      *            enclosed by double quotes will be treated as a single term.
2842      * @param selection Filter on the query as an SQL WHERE statement
2843      * @param selectionArgs Args to replace any '?'s in the selection
2844      * @param orderBy How to order the rows as an SQL ORDER BY statement
2845      * @return A Cursor of instances matching the selection
2846      */
queryInstances(ContentResolver cr, String[] projection, long begin, long end, String searchQuery, String selection, String[] selectionArgs, String orderBy)2847     public static final Cursor queryInstances(ContentResolver cr, String[] projection, long begin,
2848             long end, String searchQuery, String selection, String[] selectionArgs, String orderBy)
2849             {
2850         Uri.Builder builder = Instances.CONTENT_SEARCH_URI.buildUpon();
2851         ContentUris.appendId(builder, begin);
2852         ContentUris.appendId(builder, end);
2853         builder = builder.appendPath(searchQuery);
2854         if (TextUtils.isEmpty(selection)) {
2855             selection = WHERE_CALENDARS_SELECTED;
2856             selectionArgs = WHERE_CALENDARS_ARGS;
2857         } else {
2858             selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED;
2859             if (selectionArgs != null && selectionArgs.length > 0) {
2860                 selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1);
2861                 selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0];
2862             } else {
2863                 selectionArgs = WHERE_CALENDARS_ARGS;
2864             }
2865         }
2866         return cr.query(builder.build(), projection, selection, selectionArgs,
2867                 orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
2868     }
2869 
queryInstances(long begin, long end)2870     private Cursor queryInstances(long begin, long end) {
2871         Uri url = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_URI, begin + "/" + end);
2872         return mResolver.query(url, null, null, null, null);
2873     }
2874 
2875     protected static class MockProvider extends ContentProvider {
2876 
2877         private String mAuthority;
2878 
2879         private int mNumItems = 0;
2880 
MockProvider(String authority)2881         public MockProvider(String authority) {
2882             mAuthority = authority;
2883         }
2884 
2885         @Override
onCreate()2886         public boolean onCreate() {
2887             return true;
2888         }
2889 
2890         @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)2891         public Cursor query(Uri uri, String[] projection, String selection,
2892                 String[] selectionArgs, String sortOrder) {
2893             return new MatrixCursor(new String[]{ "_id" }, 0);
2894         }
2895 
2896         @Override
getType(Uri uri)2897         public String getType(Uri uri) {
2898             throw new UnsupportedOperationException();
2899         }
2900 
2901         @Override
insert(Uri uri, ContentValues values)2902         public Uri insert(Uri uri, ContentValues values) {
2903             mNumItems++;
2904             return Uri.parse("content://" + mAuthority + "/" + mNumItems);
2905         }
2906 
2907         @Override
delete(Uri uri, String selection, String[] selectionArgs)2908         public int delete(Uri uri, String selection, String[] selectionArgs) {
2909             return 0;
2910         }
2911 
2912         @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)2913         public int update(Uri uri, ContentValues values, String selection,
2914                 String[] selectionArgs) {
2915             return 0;
2916         }
2917     }
2918 
cleanCalendarDataTable(SQLiteOpenHelper helper)2919     private void cleanCalendarDataTable(SQLiteOpenHelper helper) {
2920         if (null == helper) {
2921             return;
2922         }
2923         SQLiteDatabase db = helper.getWritableDatabase();
2924         db.execSQL("DELETE FROM CalendarCache;");
2925     }
2926 
testGetAndSetTimezoneDatabaseVersion()2927     public void testGetAndSetTimezoneDatabaseVersion() throws CalendarCache.CacheException {
2928         CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2929         cleanCalendarDataTable(helper);
2930         CalendarCache cache = new CalendarCache(helper);
2931 
2932         boolean hasException = false;
2933         try {
2934             String value = cache.readData(null);
2935         } catch (CalendarCache.CacheException e) {
2936             hasException = true;
2937         }
2938         assertTrue(hasException);
2939 
2940         assertNull(cache.readTimezoneDatabaseVersion());
2941 
2942         cache.writeTimezoneDatabaseVersion("1234");
2943         assertEquals("1234", cache.readTimezoneDatabaseVersion());
2944 
2945         cache.writeTimezoneDatabaseVersion("5678");
2946         assertEquals("5678", cache.readTimezoneDatabaseVersion());
2947     }
2948 
checkEvent(int eventId, String title, long dtStart, long dtEnd, boolean allDay)2949     private void checkEvent(int eventId, String title, long dtStart, long dtEnd, boolean allDay) {
2950         Uri uri = Uri.parse("content://" + CalendarContract.AUTHORITY + "/events");
2951         Log.i(TAG, "Looking for EventId = " + eventId);
2952 
2953         Cursor cursor = mResolver.query(uri, null, null, null, null);
2954         assertEquals(1, cursor.getCount());
2955 
2956         int colIndexTitle = cursor.getColumnIndex(CalendarContract.Events.TITLE);
2957         int colIndexDtStart = cursor.getColumnIndex(CalendarContract.Events.DTSTART);
2958         int colIndexDtEnd = cursor.getColumnIndex(CalendarContract.Events.DTEND);
2959         int colIndexAllDay = cursor.getColumnIndex(CalendarContract.Events.ALL_DAY);
2960         if (!cursor.moveToNext()) {
2961             Log.e(TAG,"Could not find inserted event");
2962             assertTrue(false);
2963         }
2964         assertEquals(title, cursor.getString(colIndexTitle));
2965         assertEquals(dtStart, cursor.getLong(colIndexDtStart));
2966         assertEquals(dtEnd, cursor.getLong(colIndexDtEnd));
2967         assertEquals(allDay, (cursor.getInt(colIndexAllDay) != 0));
2968         cursor.close();
2969     }
2970 
testChangeTimezoneDB()2971     public void testChangeTimezoneDB() {
2972         int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2973 
2974         Cursor cursor = mResolver
2975                 .query(CalendarContract.Events.CONTENT_URI, null, null, null, null);
2976         assertEquals(0, cursor.getCount());
2977         cursor.close();
2978 
2979         EventInfo[] events = { new EventInfo("normal0",
2980                                         "2008-05-01T00:00:00",
2981                                         "2008-05-02T00:00:00",
2982                                         false,
2983                                         DEFAULT_TIMEZONE) };
2984 
2985         Uri uri = insertEvent(calId, events[0]);
2986         assertNotNull(uri);
2987 
2988         // check the inserted event
2989         checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
2990 
2991         // inject a new time zone
2992         getProvider().doProcessEventRawTimes(TIME_ZONE_AMERICA_ANCHORAGE,
2993                 MOCK_TIME_ZONE_DATABASE_VERSION);
2994 
2995         // check timezone database version
2996         assertEquals(MOCK_TIME_ZONE_DATABASE_VERSION, getProvider().getTimezoneDatabaseVersion());
2997 
2998         // check that the inserted event has *not* been updated
2999         checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
3000     }
3001 
3002     public static final Uri PROPERTIES_CONTENT_URI =
3003             Uri.parse("content://" + CalendarContract.AUTHORITY + "/properties");
3004 
testGetProviderProperties()3005     public void testGetProviderProperties() throws CalendarCache.CacheException {
3006         CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
3007         cleanCalendarDataTable(helper);
3008         CalendarCache cache = new CalendarCache(helper);
3009 
3010         cache.writeTimezoneDatabaseVersion("2010k");
3011         cache.writeTimezoneInstances("America/Denver");
3012         cache.writeTimezoneInstancesPrevious("America/Los_Angeles");
3013         cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
3014 
3015         Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null, null, null, null);
3016         assertEquals(4, cursor.getCount());
3017 
3018         final int keyColumnIndex = cursor.getColumnIndex(CalendarCache.COLUMN_NAME_KEY);
3019         final int valueColumnIndex = cursor.getColumnIndex(CalendarCache.COLUMN_NAME_VALUE);
3020         Map<String, String> map = new HashMap<String, String>();
3021 
3022         while (cursor.moveToNext()) {
3023             String key = cursor.getString(keyColumnIndex);
3024             String value = cursor.getString(valueColumnIndex);
3025             map.put(key, value);
3026         }
3027 
3028         assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION));
3029         assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_TYPE));
3030         assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES));
3031         assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS));
3032 
3033         assertEquals("2010k", map.get(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION));
3034         assertEquals("America/Denver", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES));
3035         assertEquals("America/Los_Angeles", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS));
3036         assertEquals(CalendarCache.TIMEZONE_TYPE_AUTO, map.get(CalendarCache.KEY_TIMEZONE_TYPE));
3037 
3038         cursor.close();
3039     }
3040 
testGetProviderPropertiesByKey()3041     public void testGetProviderPropertiesByKey() throws CalendarCache.CacheException {
3042         CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
3043         cleanCalendarDataTable(helper);
3044         CalendarCache cache = new CalendarCache(helper);
3045 
3046         cache.writeTimezoneDatabaseVersion("2010k");
3047         cache.writeTimezoneInstances("America/Denver");
3048         cache.writeTimezoneInstancesPrevious("America/Los_Angeles");
3049         cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
3050 
3051         checkValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE);
3052         checkValueForKey("2010k", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
3053         checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES);
3054         checkValueForKey("America/Los_Angeles", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
3055     }
3056 
checkValueForKey(String value, String key)3057     private void checkValueForKey(String value, String key) {
3058         Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null,
3059                 "key=?", new String[] {key}, null);
3060 
3061         assertEquals(1, cursor.getCount());
3062         assertTrue(cursor.moveToFirst());
3063         assertEquals(cursor.getString(cursor.getColumnIndex(CalendarCache.COLUMN_NAME_KEY)),
3064                 key);
3065         assertEquals(cursor.getString(cursor.getColumnIndex(CalendarCache.COLUMN_NAME_VALUE)),
3066                 value);
3067 
3068         cursor.close();
3069     }
3070 
testUpdateProviderProperties()3071     public void testUpdateProviderProperties() throws CalendarCache.CacheException {
3072         CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
3073         cleanCalendarDataTable(helper);
3074         CalendarCache cache = new CalendarCache(helper);
3075 
3076         String localTimezone = TimeZone.getDefault().getID();
3077 
3078         // Set initial value
3079         cache.writeTimezoneDatabaseVersion("2010k");
3080 
3081         updateValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
3082         checkValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
3083 
3084         // Set initial values
3085         cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
3086         cache.writeTimezoneInstances("America/Chicago");
3087         cache.writeTimezoneInstancesPrevious("America/Denver");
3088 
3089         updateValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE);
3090         checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
3091         checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
3092 
3093         updateValueForKey(CalendarCache.TIMEZONE_TYPE_HOME, CalendarCache.KEY_TIMEZONE_TYPE);
3094         checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES);
3095         checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
3096 
3097         // Set initial value
3098         cache.writeTimezoneInstancesPrevious("");
3099         updateValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
3100         checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
3101         checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
3102     }
3103 
updateValueForKey(String value, String key)3104     private void updateValueForKey(String value, String key) {
3105         ContentValues contentValues = new ContentValues();
3106         contentValues.put(CalendarCache.COLUMN_NAME_VALUE, value);
3107 
3108         int result = mResolver.update(PROPERTIES_CONTENT_URI,
3109                 contentValues,
3110                 CalendarCache.COLUMN_NAME_KEY + "=?",
3111                 new String[] {key});
3112 
3113         assertEquals(1, result);
3114     }
3115 
3116     /**
3117      * Verifies that the number of defined calendars meets expectations.
3118      *
3119      * @param expectedCount The number of calendars we expect to find.
3120      */
checkCalendarCount(int expectedCount)3121     private void checkCalendarCount(int expectedCount) {
3122         Cursor cursor = mResolver.query(mCalendarsUri,
3123                 null /* projection */,
3124                 null /* selection */,
3125                 null /* selectionArgs */,
3126                 null /* sortOrder */);
3127         assertEquals(expectedCount, cursor.getCount());
3128         cursor.close();
3129     }
3130 
checkCalendarExists(int calId)3131     private void checkCalendarExists(int calId) {
3132         assertTrue(isCalendarExists(calId));
3133     }
3134 
checkCalendarDoesNotExists(int calId)3135     private void checkCalendarDoesNotExists(int calId) {
3136         assertFalse(isCalendarExists(calId));
3137     }
3138 
isCalendarExists(int calId)3139     private boolean isCalendarExists(int calId) {
3140         Cursor cursor = mResolver.query(mCalendarsUri,
3141                 new String[] {Calendars._ID},
3142                 null /* selection */,
3143                 null /* selectionArgs */,
3144                 null /* sortOrder */);
3145         boolean found = false;
3146         while (cursor.moveToNext()) {
3147             if (calId == cursor.getInt(0)) {
3148                 found = true;
3149                 break;
3150             }
3151         }
3152         cursor.close();
3153         return found;
3154     }
3155 
testDeleteAllCalendars()3156     public void testDeleteAllCalendars() {
3157         checkCalendarCount(0);
3158 
3159         insertCal("Calendar1", "America/Los_Angeles");
3160         insertCal("Calendar2", "America/Los_Angeles");
3161 
3162         checkCalendarCount(2);
3163 
3164         deleteMatchingCalendars(null /* selection */, null /* selectionArgs*/);
3165         checkCalendarCount(0);
3166     }
3167 
testDeleteCalendarsWithSelection()3168     public void testDeleteCalendarsWithSelection() {
3169         checkCalendarCount(0);
3170 
3171         int calId1 = insertCal("Calendar1", "America/Los_Angeles");
3172         int calId2 = insertCal("Calendar2", "America/Los_Angeles");
3173 
3174         checkCalendarCount(2);
3175         checkCalendarExists(calId1);
3176         checkCalendarExists(calId2);
3177 
3178         deleteMatchingCalendars(Calendars._ID + "=" + calId2, null /* selectionArgs*/);
3179         checkCalendarCount(1);
3180         checkCalendarExists(calId1);
3181         checkCalendarDoesNotExists(calId2);
3182     }
3183 
testDeleteCalendarsWithSelectionAndArgs()3184     public void testDeleteCalendarsWithSelectionAndArgs() {
3185         checkCalendarCount(0);
3186 
3187         int calId1 = insertCal("Calendar1", "America/Los_Angeles");
3188         int calId2 = insertCal("Calendar2", "America/Los_Angeles");
3189 
3190         checkCalendarCount(2);
3191         checkCalendarExists(calId1);
3192         checkCalendarExists(calId2);
3193 
3194         deleteMatchingCalendars(Calendars._ID + "=?",
3195                 new String[] { Integer.toString(calId2) });
3196         checkCalendarCount(1);
3197         checkCalendarExists(calId1);
3198         checkCalendarDoesNotExists(calId2);
3199 
3200         deleteMatchingCalendars(Calendars._ID + "=?" + " AND " + Calendars.NAME + "=?",
3201                 new String[] { Integer.toString(calId1), "Calendar1" });
3202         checkCalendarCount(0);
3203     }
3204 
testGetColumnIndex_IsPrimary()3205     public void testGetColumnIndex_IsPrimary() {
3206         checkCalendarCount(0);
3207         int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
3208 
3209         String[] projection = new String[] {
3210             Calendars.ACCOUNT_NAME,
3211             Calendars.CALENDAR_DISPLAY_NAME,
3212             Calendars.OWNER_ACCOUNT,
3213             Calendars.IS_PRIMARY
3214         };
3215         String selection = "((" + Calendars.ACCOUNT_NAME + " = ? ))";
3216         String[] selectionArgs = new String[] {
3217             DEFAULT_ACCOUNT
3218         };
3219         Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs,
3220                 null);
3221         assertNotNull(cursor);
3222         assertEquals(3, cursor.getColumnIndex(Calendars.IS_PRIMARY));
3223         cursor.close();
3224         deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/);
3225         checkCalendarCount(0);
3226     }
3227 
testGetIsPrimary_ForEvents()3228     public void testGetIsPrimary_ForEvents() {
3229         checkCalendarCount(0);
3230         int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
3231 
3232         final String START = "2008-05-01T00:00:00";
3233         final String END = "2008-05-01T20:00:00";
3234         EventInfo event = new EventInfo("search orange",
3235                 START,
3236                 END,
3237                 false /* allDay */,
3238                 DEFAULT_TIMEZONE);
3239 
3240         insertEvent(calendarId0, event);
3241 
3242         String[] projection = new String[] {
3243                 Calendars.IS_PRIMARY
3244         };
3245         String selection =
3246                 "((" + Calendars.IS_PRIMARY + " = ? OR " + Calendars.ACCOUNT_NAME + " = ?))";
3247         String[] selectionArgs = new String[] {
3248                 "1", DEFAULT_ACCOUNT
3249         };
3250         Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs,
3251                 null);
3252         assertNotNull(cursor);
3253         cursor.moveToLast();
3254         assertEquals(1, cursor.getCount());
3255         assertEquals(1, cursor.getInt(cursor.getColumnIndex(Calendars.IS_PRIMARY)));
3256         cursor.close();
3257         deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/);
3258         checkCalendarCount(0);
3259     }
3260 
testGetIsNotPrimary_ForEvents()3261     public void testGetIsNotPrimary_ForEvents() {
3262         checkCalendarCount(0);
3263         int calendarId0 = insertNonPrimaryCal("Calendar0", DEFAULT_TIMEZONE, DEFAULT_ACCOUNT);
3264 
3265         final String START = "2008-05-01T00:00:00";
3266         final String END = "2008-05-01T20:00:00";
3267         EventInfo event = new EventInfo("search orange",
3268                 START,
3269                 END,
3270                 false /* allDay */,
3271                 DEFAULT_TIMEZONE);
3272 
3273         insertEvent(calendarId0, event);
3274 
3275         String[] projection = new String[] {
3276                 Calendars.IS_PRIMARY
3277         };
3278         String selection = "((" + Calendars.ACCOUNT_NAME + " = ? ))";
3279         String[] selectionArgs = new String[] {
3280                 DEFAULT_ACCOUNT
3281         };
3282         Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs,
3283                 null);
3284         assertNotNull(cursor);
3285         cursor.moveToLast();
3286         assertEquals(1, cursor.getCount());
3287         assertEquals(0, cursor.getInt(cursor.getColumnIndex(Calendars.IS_PRIMARY)));
3288         cursor.close();
3289         deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/);
3290         checkCalendarCount(0);
3291     }
3292 
testGetColumnIndex_Count()3293     public void testGetColumnIndex_Count() {
3294         checkCalendarCount(0);
3295         int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
3296 
3297         String[] projection = new String[] {
3298             BaseColumns._COUNT
3299         };
3300         String selection = "((" + Calendars.ACCOUNT_NAME + " = ? ))";
3301         String[] selectionArgs = new String[] {
3302             DEFAULT_ACCOUNT
3303         };
3304         Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs,
3305                 null);
3306         assertNotNull(cursor);
3307         assertEquals(0, cursor.getColumnIndex(BaseColumns._COUNT));
3308         cursor.close();
3309         deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/);
3310         checkCalendarCount(0);
3311     }
3312 
testEnterpriseInstancesGetCorrectValue()3313     public void testEnterpriseInstancesGetCorrectValue() {
3314         final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE);
3315         insertWorkEvent(WORK_EVENT_TITLE, calendarId,
3316                 WORK_EVENT_DTSTART, WORK_EVENT_DTEND);
3317         insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId,
3318                 WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY);
3319         // Assume cross profile uri access is allowed by policy and settings.
3320         MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true);
3321 
3322         Uri.Builder builder = Instances.ENTERPRISE_CONTENT_URI.buildUpon();
3323         ContentUris.appendId(builder, WORK_EVENT_DTSTART - DateUtils.YEAR_IN_MILLIS);
3324         ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS);
3325         String[] projection = new String[]{
3326                 Instances.TITLE,
3327                 Instances.CALENDAR_ID,
3328                 Instances.DTSTART,
3329                 Instances.CALENDAR_COLOR,
3330         };
3331         Cursor cursor = mResolver.query(
3332                 builder.build(),
3333                 projection, null, null, null);
3334 
3335         // Test the return cursor is correct when the all checks are met.
3336         assertNotNull(cursor);
3337         assertEquals(1, cursor.getCount());
3338         cursor.moveToFirst();
3339         assertEquals(WORK_EVENT_TITLE, cursor.getString(0));
3340         assertEquals(calendarId, cursor.getLong(1));
3341         assertEquals(WORK_EVENT_DTSTART, cursor.getLong(2));
3342         assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(3));
3343         cursor.close();
3344 
3345         cleanupEnterpriseTestForEvents(calendarId, 2);
3346         cleanupEnterpriseTestForCalendars(1);
3347     }
3348 
testEnterpriseInstancesContentSearch()3349     public void testEnterpriseInstancesContentSearch() {
3350         final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE);
3351         insertWorkEvent(WORK_EVENT_TITLE, calendarId,
3352                 WORK_EVENT_DTSTART, WORK_EVENT_DTEND);
3353         insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId,
3354                 WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY);
3355         // Assume cross profile uri access is allowed by policy and settings.
3356         MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true);
3357 
3358         Uri.Builder builder = Instances.ENTERPRISE_CONTENT_SEARCH_URI.buildUpon();
3359         ContentUris.appendId(builder, WORK_EVENT_DTSTART - DateUtils.YEAR_IN_MILLIS);
3360         ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS);
3361         builder = builder.appendPath(WORK_EVENT_TITLE /* search query */);
3362         Cursor cursor = mResolver.query(
3363                 builder.build(),
3364                 null, null, null, null);
3365         // There is only one event that meets the search criteria.
3366         assertNotNull(cursor);
3367         assertEquals(1, cursor.getCount());
3368 
3369         builder = Instances.ENTERPRISE_CONTENT_SEARCH_URI.buildUpon();
3370         ContentUris.appendId(builder, WORK_EVENT_DTSTART_STANDBY - DateUtils.YEAR_IN_MILLIS);
3371         ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS);
3372         builder = builder.appendPath(WORK_EVENT_DESCRIPTION /* search query */);
3373         cursor = mResolver.query(
3374                 builder.build(),
3375                 null, null, null, null);
3376         // There are two events that meet the search criteria.
3377         assertNotNull(cursor);
3378         assertEquals(2, cursor.getCount());
3379         cursor.close();
3380 
3381         cleanupEnterpriseTestForEvents(calendarId, 2);
3382         cleanupEnterpriseTestForCalendars(1);
3383     }
3384 
testEnterpriseEventsGetCorrectValue()3385     public void testEnterpriseEventsGetCorrectValue() {
3386         final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE);
3387         final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId,
3388                 WORK_EVENT_DTSTART, WORK_EVENT_DTEND);
3389         insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId,
3390                 WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY);
3391         // Assume cross profile uri access is allowed by policy and settings.
3392         MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true);
3393 
3394         String selection = "(" + Events.TITLE + " = ? )";
3395         String[] selectionArgs = new String[]{
3396                 WORK_EVENT_TITLE
3397         };
3398         String[] projection = new String[]{
3399                 Events._ID,
3400                 Events.TITLE,
3401                 Events.CALENDAR_ID,
3402                 Events.DTSTART,
3403                 Calendars.CALENDAR_COLOR
3404         };
3405         Cursor cursor = mResolver.query(
3406                 Events.ENTERPRISE_CONTENT_URI,
3407                 projection, selection, selectionArgs, null);
3408 
3409         // Test the return cursor is correct when the all checks are met.
3410         assertNotNull(cursor);
3411         assertEquals(1, cursor.getCount());
3412         cursor.moveToFirst();
3413         assertEquals(idToTest, cursor.getLong(0));
3414         assertEquals(WORK_EVENT_TITLE, cursor.getString(1));
3415         assertEquals(calendarId, cursor.getLong(2));
3416         assertEquals(WORK_EVENT_DTSTART, cursor.getLong(3));
3417         assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(4));
3418         cursor.close();
3419 
3420         cleanupEnterpriseTestForEvents(calendarId, 2);
3421         cleanupEnterpriseTestForCalendars(1);
3422     }
3423 
testEnterpriseEventsGetCorrectValueWithId()3424     public void testEnterpriseEventsGetCorrectValueWithId() {
3425         final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE);
3426         final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId,
3427                 WORK_EVENT_DTSTART, WORK_EVENT_DTEND);
3428         insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId,
3429                 WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY);
3430         // Assume cross profile uri access is allowed by policy and settings.
3431         MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true);
3432 
3433         // Test ENTERPRISE_CONTENT_URI_ID.
3434         String[] projection = new String[]{
3435                 Events._ID,
3436                 Events.TITLE,
3437                 Events.CALENDAR_ID,
3438                 Events.DTSTART,
3439                 Calendars.CALENDAR_COLOR
3440         };
3441         final Cursor cursor = mResolver.query(
3442                 ContentUris.withAppendedId(Events.ENTERPRISE_CONTENT_URI, idToTest),
3443                 projection, null, null, null);
3444 
3445         assertNotNull(cursor);
3446         assertEquals(1, cursor.getCount());
3447         cursor.moveToFirst();
3448         assertEquals(idToTest, cursor.getLong(0));
3449         assertEquals(WORK_EVENT_TITLE, cursor.getString(1));
3450         assertEquals(calendarId, cursor.getLong(2));
3451         assertEquals(WORK_EVENT_DTSTART, cursor.getLong(3));
3452         assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(4));
3453         assertEquals(1, cursor.getInt(2));
3454         cursor.close();
3455 
3456         cleanupEnterpriseTestForEvents(calendarId, 2);
3457         cleanupEnterpriseTestForCalendars(1);
3458     }
3459 
testEnterpriseEventsProjectionCalibration()3460     public void testEnterpriseEventsProjectionCalibration() {
3461         final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE);
3462         final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId,
3463                 WORK_EVENT_DTSTART, WORK_EVENT_DTEND);
3464         insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId,
3465                 WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY);
3466         // Assume cross profile uri access is allowed by policy and settings.
3467         MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true);
3468 
3469         // Test all whitelisted columns are returned when projection is empty.
3470         String selection = "(" + Events.TITLE + " = ? )";
3471         String[] selectionArgs = new String[]{
3472                 WORK_EVENT_TITLE
3473         };
3474         final Cursor cursor = mResolver.query(
3475                 Events.ENTERPRISE_CONTENT_URI,
3476                 new String[] {}, selection, selectionArgs, null);
3477 
3478         assertNotNull(cursor);
3479         assertEquals(1, cursor.getCount());
3480         cursor.moveToFirst();
3481         for (String column : CrossProfileCalendarHelper.EVENTS_TABLE_WHITELIST) {
3482             final int index = cursor.getColumnIndex(column);
3483             assertTrue(index != -1);
3484         }
3485         assertEquals(idToTest, cursor.getLong(
3486                 cursor.getColumnIndex(Events._ID)));
3487         assertEquals(calendarId, cursor.getLong(
3488                 cursor.getColumnIndex(Events.CALENDAR_ID)));
3489         assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(
3490                 cursor.getColumnIndex(Calendars.CALENDAR_COLOR)));
3491         assertEquals(1, cursor.getInt(
3492                 cursor.getColumnIndex(Calendars.IS_PRIMARY)));
3493         cursor.close();
3494 
3495         cleanupEnterpriseTestForEvents(calendarId, 2);
3496         cleanupEnterpriseTestForCalendars(1);
3497     }
3498 
cleanupEnterpriseTestForEvents(long calendarId, int numToDelete)3499     private void cleanupEnterpriseTestForEvents(long calendarId, int numToDelete) {
3500         // Selection arguments must be provided when deleting events.
3501         final int numDeleted = mWorkProfileProvider.delete(Events.CONTENT_URI,
3502                 "(" + Events.CALENDAR_ID + " = ? )",
3503                 new String[]{String.valueOf(calendarId)});
3504         assertTrue(numDeleted == numToDelete);
3505     }
3506 
insertWorkEvent(String eventTitle, long calendarId, long dtStart, long dtEnd)3507     private long insertWorkEvent(String eventTitle, long calendarId, long dtStart, long dtEnd) {
3508         final ContentValues cv = new ContentValues();
3509         cv.put(Events.TITLE, eventTitle);
3510         cv.put(Events.CALENDAR_ID, calendarId);
3511         cv.put(Events.DESCRIPTION, WORK_EVENT_DESCRIPTION);
3512         cv.put(Events.EVENT_LOCATION, WORK_EVENT_LOCATION);
3513         cv.put(Events.EVENT_COLOR, WORK_EVENT_COLOR);
3514         cv.put(Events.DTSTART, dtStart);
3515         cv.put(Events.DTEND, dtEnd);
3516         cv.put(Events.EVENT_TIMEZONE, WORK_DEFAULT_TIMEZONE);
3517         final Uri uri = mWorkProfileProvider.insert(Events.CONTENT_URI, cv);
3518         return Long.parseLong(uri.getLastPathSegment());
3519     }
3520 
testEnterpriseCalendarGetCorrectValue()3521     public void testEnterpriseCalendarGetCorrectValue() {
3522         final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE);
3523         insertWorkCalendar(WORK_CALENDAR_TITLE_STANDBY);
3524         // Assume cross profile uri access is allowed by policy and settings.
3525         MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true);
3526 
3527         // Test the return cursor is correct when the all checks are met.
3528         String selection = "(" + Calendars.CALENDAR_DISPLAY_NAME + " = ? )";
3529         String[] selectionArgs = new String[] {
3530                 WORK_CALENDAR_TITLE
3531         };
3532         String[] projection = new String[] {
3533                 Calendars._ID,
3534                 Calendars.CALENDAR_COLOR
3535         };
3536         Cursor cursor = mResolver.query(
3537                 Calendars.ENTERPRISE_CONTENT_URI,
3538                 projection, selection, selectionArgs, null);
3539 
3540         assertNotNull(cursor);
3541         assertEquals(1, cursor.getCount());
3542         cursor.moveToFirst();
3543         assertEquals(idToTest, cursor.getLong(0));
3544         assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(1));
3545 
3546         cleanupEnterpriseTestForCalendars(2);
3547     }
3548 
testEnterpriseCalendarGetCorrectValueWithId()3549     public void testEnterpriseCalendarGetCorrectValueWithId() {
3550         final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE);
3551         insertWorkCalendar(WORK_CALENDAR_TITLE_STANDBY);
3552         // Assume cross profile uri access is allowed by policy and settings.
3553         MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true);
3554 
3555         // Test Calendars.ENTERPRISE_CONTENT_URI with id.
3556         String[] projection = new String[] {
3557                 Calendars._ID,
3558                 Calendars.CALENDAR_COLOR
3559         };
3560         final Cursor cursor = mResolver.query(
3561                 ContentUris.withAppendedId(Calendars.ENTERPRISE_CONTENT_URI, idToTest),
3562                 projection, null, null, null);
3563 
3564         assertNotNull(cursor);
3565         assertEquals(1, cursor.getCount());
3566         cursor.moveToFirst();
3567         assertEquals(idToTest, cursor.getLong(0));
3568         assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(1));
3569         cursor.close();
3570 
3571         cleanupEnterpriseTestForCalendars(2);
3572     }
3573 
testEnterpriseCalendarsProjectionCalibration()3574     public void testEnterpriseCalendarsProjectionCalibration() {
3575         final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE);
3576         // Assume cross profile uri access is allowed by policy and settings.
3577         MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true);
3578 
3579         // Test all whitelisted columns are returned when projection is empty.
3580         final Cursor cursor = mResolver.query(
3581                 Calendars.ENTERPRISE_CONTENT_URI,
3582                 new String[] {}, null, null, null);
3583 
3584         assertNotNull(cursor);
3585         assertEquals(1, cursor.getCount());
3586         cursor.moveToFirst();
3587         for (String column : CrossProfileCalendarHelper.CALENDARS_TABLE_WHITELIST) {
3588             final int index = cursor.getColumnIndex(column);
3589             assertTrue(index != -1);
3590         }
3591         assertEquals(idToTest, cursor.getLong(
3592                 cursor.getColumnIndex(Calendars._ID)));
3593         assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(
3594                 cursor.getColumnIndex(Calendars.CALENDAR_COLOR)));
3595         cursor.close();
3596 
3597         cleanupEnterpriseTestForCalendars(1);
3598     }
3599 
testEnterpriseCalendarsNonWhitelistedProjection()3600     public void testEnterpriseCalendarsNonWhitelistedProjection() {
3601         // Test SecurityException is thrown there is non-whitelisted column in the projection.
3602         try {
3603             String[] projection = new String[] {
3604                     Calendars._ID,
3605                     Calendars.CALENDAR_DISPLAY_NAME,
3606                     Calendars.CALENDAR_COLOR,
3607                     Calendars.OWNER_ACCOUNT
3608             };
3609             mResolver.query(
3610                     Calendars.ENTERPRISE_CONTENT_URI,
3611                     projection, null, null, null);
3612             fail("IllegalArgumentException is not thrown when querying non-whitelisted columns");
3613         } catch (IllegalArgumentException e) {
3614         }
3615     }
3616 
testEnterpriseCalendarsNotAllowed()3617     public void testEnterpriseCalendarsNotAllowed() {
3618         insertWorkCalendar(WORK_CALENDAR_TITLE);
3619         // Assume cross profile uri access is not allowed by policy or disabled in settings.
3620         MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(false);
3621 
3622         // Throw exception if cross profile calendar is disabled in settings.
3623         try {
3624             final Cursor cursor = mResolver.query(
3625                     Calendars.ENTERPRISE_CONTENT_URI,
3626                     new String[]{}, null, null, null);
3627             fail("Unsupported operation exception should have been raised.");
3628         } catch (UnsupportedOperationException e) {
3629             // Exception expected.
3630         }
3631         cleanupEnterpriseTestForCalendars(1);
3632     }
3633 
3634     // Remove the two inserted calendars.
cleanupEnterpriseTestForCalendars(int numToDelete)3635     private void cleanupEnterpriseTestForCalendars(int numToDelete) {
3636         final int numDeleted =  mWorkProfileProvider.delete(Calendars.CONTENT_URI, null, null);
3637         assertTrue(numDeleted == numToDelete);
3638     }
3639 
insertWorkCalendar(String displayName)3640     private long insertWorkCalendar(String displayName) {
3641         final ContentValues cv = new ContentValues();
3642         cv.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
3643         cv.put(Calendars.OWNER_ACCOUNT, DEFAULT_ACCOUNT);
3644         cv.put(Calendars.ACCOUNT_NAME, DEFAULT_ACCOUNT);
3645         cv.put(Calendars.CALENDAR_DISPLAY_NAME, displayName);
3646         cv.put(Calendars.CALENDAR_COLOR, WORK_CALENDAR_COLOR);
3647         cv.put(Calendars.CALENDAR_TIME_ZONE, WORK_DEFAULT_TIMEZONE);
3648         final Uri uri = mWorkProfileProvider.insert(
3649                 addSyncQueryParams(Calendars.CONTENT_URI, "local_account",
3650                         CalendarContract.ACCOUNT_TYPE_LOCAL), cv);
3651         return Long.parseLong(uri.getLastPathSegment());
3652     }
3653 }
3654 
3655