Grokking Android

Getting Down to the Nitty Gritty of Android Development

Android’s CalendarContract Content Provider

By

Android developers have been longing for an official Calendar app and content provider since Android has been released. With the release of Ice Cream Sandwich Google has finally added this feature to our tools list. Now we developers can use the Calendar app from within our Activities using Intents or we can access the data by making use of the new CalendarContract content provider.

CalendarContracts entities

Before I explain how to use the calendar I first present the calendar's data model. It consists of calendars, events, event instances, event attendees and reminders.

First we obviously have calendars. The plural is no accident, because each user can manage multiple calendars on her device. For example her personal calendar, her work calendar, a calendar of her colleague and one for her sports team. Android uses colors to distinguish between different calendars in the Calendar app:

Events of multiple accounts
Events of multiple accounts
Account selection when adding an event
Account selection when adding an event

For each calendar you can have multiple events. But each event belongs to exactly one calendar only. If the event is visible twice (e.g. in the private and in the business account of the user), the event is stored twice, each belonging to their respective calendars.

Many events are recurring. So in addition to events there are also event instances. An instance represents the specific event that takes place at just this one point in time. For a single event Android creates one instances entry, for recurring events Android creates as many instance entries as there are occurrences of this event.

Events can of course have multiple attendees as well as multiple reminders.

Furthermore the data model contains some helper tables that are only relevant for sync adapters. I don't deal with these in this blog post.

What's weird is that attendees belong to the event and not to the event instances. What happens if users want to cancel one instance of a recurring event? What if they want to add an attendee for just one event? For example changing the speaker of a recurring User Group talk? The solution chosen here is that another entry is added to the events table with the field Events.ORIGINAL_ID referring to the id of the base event. This is also the case for event instances for which only the time has changed - even though this information is already part of the instances table.

What are sync adapters

Sync adapters sync data of specific accounts to a backend. Sync adapters are usually an integral part of the app, that needs them. Thus users usually do not install them on their own.

In this blog post I do not deal in detail with sync adapters. But you should know that sync adapters possess more privileges and can access more tables and fields than normal apps. The most important privilege is that only sync adapters can create calendar accounts.

Of course a sync adapter has these privileges only for its account type. An adapter cannot mess with the privileged data of another account.

Sync adapters don't deal with calendar data alone. They also sync contact data or any data specific to your app that has to be synchronized between the device and a backend.

You can find a list of available sync adapters in your Settings app in the section "Accounts":

Sync adapters in the settings app
Sync adapters in the settings app

Using your Google Calendar within the emulator

To test your calendar-related code within the emulator, you first need to have a calendar. The best is to sync with an existing calendar of yours - and the easiest way to do this, is to sync with your Google calendar.

I have written another post that explains how to sync with your Google Calendar.

The Calendar content provider

To use the Calendar content provider you need to declare the necessary permissions within your manifest file first:


<uses-permission 
      android:name="android.permission.READ_CALENDAR"/>
<uses-permission 
      android:name="android.permission.WRITE_CALENDAR"/>

Keep in mind that the Calendar is part of the official API only from Ice Cream Sandwich onwards. The constants, defined in CalendarContract and its many inner classes are not available prior to API version 14. All code samples shown here need an API-level of 14 or higher as build target and the value for the android:minSdkVersion attribute of the &lt;uses-sdk&gt; element in the manifest file.

The content provider itself probably is available on older versions - though you cannot rely on this. For more on this see the section Calendar-usage before Ice Cream Sandwich.

Accessing calendars

If your app doesn't act as a sync adapter chances are that you never have to deal with the calendars itself. Other than query for the correct id that is. The thing you are most likely to do is add, delete or change events. But for this you need to know to which calendar to add the event.

So let's start by reading all calendars that are available on a device:


String[] projection = 
      new String[]{
            Calendars._ID, 
            Calendars.NAME, 
            Calendars.ACCOUNT_NAME, 
            Calendars.ACCOUNT_TYPE};
Cursor calCursor = 
      getContentResolver().
            query(Calendars.CONTENT_URI, 
                  projection, 
                  Calendars.VISIBLE + " = 1", 
                  null, 
                  Calendars._ID + " ASC");
if (calCursor.moveToFirst()) {
   do {
      long id = calCursor.getLong(0);
      String displayName = calCursor.getString(1);
      // ...
   } while (calCursor.moveToNext());
}

Of course you can also change calendars using the content provider. But here it starts getting tricky. That's because apart from reading data, the access to calendars is limited. Sync adapters can do pretty much anything with calendars they own, but normal apps cannot do much at all. Of course they cannot delete calendars, but they also cannot create calendars. They only can change some trivial values like the name of the calendar.

There is one exception to this rule: You can create local calendars that do not get synced. So the following code shows how to create a local calendar which you will use later on for manipulating events:


ContentValues values = new ContentValues();
values.put(
      Calendars.ACCOUNT_NAME, 
      MY_ACCOUNT_NAME);
values.put(
      Calendars.ACCOUNT_TYPE, 
      CalendarContract.ACCOUNT_TYPE_LOCAL);
values.put(
      Calendars.NAME, 
      "GrokkingAndroid Calendar");
values.put(
      Calendars.CALENDAR_DISPLAY_NAME, 
      "GrokkingAndroid Calendar");
values.put(
      Calendars.CALENDAR_COLOR, 
      0xffff0000);
values.put(
      Calendars.CALENDAR_ACCESS_LEVEL, 
      Calendars.CAL_ACCESS_OWNER);
values.put(
      Calendars.OWNER_ACCOUNT, 
      "some.account@googlemail.com");
values.put(
      Calendars.CALENDAR_TIME_ZONE, 
      "Europe/Berlin");
values.put(
      Calendars.SYNC_EVENTS, 
      1);
Uri.Builder builder = 
      CalendarContract.Calendars.CONTENT_URI.buildUpon(); 
builder.appendQueryParameter(
      Calendars.ACCOUNT_NAME, 
      "com.grokkingandroid");
builder.appendQueryParameter(
      Calendars.ACCOUNT_TYPE, 
      CalendarContract.ACCOUNT_TYPE_LOCAL);
builder.appendQueryParameter(
      CalendarContract.CALLER_IS_SYNCADAPTER, 
      "true");
Uri uri = 
      getContentResolver().insert(builder.build(), values);

Now this code needs some explaining. First of all I create a ContentValues object that represents the values we want to add. This is the normal way to add data to a content provider. Here I add a name, the color for displaying this calendar's data, the account it belongs to and its access rights. I also add a default time zone.

But what is not normal with the code shown above are the additional query parameters the calendar provider expects. These indicate to the provider that this code acts as a sync adapter and for which account type and account name it does this. Here the account type is ACCOUNT_TYPE_LOCAL - so this calendar will not be synced. Any other type would have to be accompanied by a full-fledged sync adapter.

Adding events using the content provider

While you cannot do anything without having at least one calendar first, you most likely want to create, change or delete events.

Since all events belong to a calendar account, I use this helper method to get the calendar id for the local calendar created in the previous section:


private long getCalendarId() { 
   String[] projection = new String[]{Calendars._ID}; 
   String selection = 
         Calendars.ACCOUNT_NAME + 
         " = ? AND " + 
         Calendars.ACCOUNT_TYPE + 
         " = ? "; 
   // use the same values as above:
   String[] selArgs = 
         new String[]{
               MY_ACCOUNT_NAME, 
               CalendarContract.ACCOUNT_TYPE_LOCAL}; 
   Cursor cursor = 
         getContentResolver().
               query(
                  Calendars.CONTENT_URI, 
                  projection, 
                  selection, 
                  selArgs, 
                  null); 
   if (cursor.moveToFirst()) { 
      return cursor.getLong(0); 
   } 
   return -1; 
}

In addition to this calendar id you also have to add at least the following fields in order to create events:

Actually the last rule is very strange. Duration in this case is not the duration of a single event. Instead it is the time span used to determine the last event of recurring events. So for single events a duration makes no sense at all. Also I consider it odd that you have to include these fields even for all-day events. But well, it's not too hard to add them. So just be warned.

The following snippet shows how to add an all-day event:


long calId = getCalendarId();
if (calId == -1) {
   // no calendar account; react meaningfully
   return;
}
Calendar cal = new GregorianCalendar(2012, 11, 14);
cal.setTimeZone(TimeZone.getTimeZone("UTC"));
cal.set(Calendar.HOUR, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
long start = cal.getTimeInMillis();
ContentValues values = new ContentValues();
values.put(Events.DTSTART, start);
values.put(Events.DTEND, start);
values.put(Events.RRULE, 
      "FREQ=DAILY;COUNT=20;BYDAY=MO,TU,WE,TH,FR;WKST=MO");
values.put(Events.TITLE, "Some title");
values.put(Events.EVENT_LOCATION, "Münster");
values.put(Events.CALENDAR_ID, calId);
values.put(Events.EVENT_TIMEZONE, "Europe/Berlin");
values.put(Events.DESCRIPTION, 
      "The agenda or some description of the event");
// reasonable defaults exist:
values.put(Events.ACCESS_LEVEL, Events.ACCESS_PRIVATE);
values.put(Events.SELF_ATTENDEE_STATUS,
      Events.STATUS_CONFIRMED);
values.put(Events.ALL_DAY, 1);
values.put(Events.ORGANIZER, "some.mail@some.address.com");
values.put(Events.GUESTS_CAN_INVITE_OTHERS, 1);
values.put(Events.GUESTS_CAN_MODIFY, 1);
values.put(Events.AVAILABILITY, Events.AVAILABILITY_BUSY);
Uri uri = 
      getContentResolver().
            insert(Events.CONTENT_URI, values);
long eventId = new Long(uri.getLastPathSegment());

If you add an event in this way, you have not yet created any alarms or set any attendes. You will have to do so in an extra step and you have to use the id of this event to do so. I will show how to add attendes and alarms later on. In preparation for this I have already extracted the id of the event.

For a lot of fields reasonable default would be inserted if you left them out. The code shown here uses them anyway so that you know what you can do and how to do it.

Since the underlying datastore doesn't suppport boolean values, the provider uses 1 for true and 0 for false. A common practise in Android when SQLite is involved. As you can see, I've set the values of ALL_DAY, GUESTS_CAN_INVITE_OTHERS and GUESTS_CAN_MODIFY to true.

The value of the rrule field looks a bit strange. First the name: "rrule" is short for recurrence rule and defines how recurring events should be inserted. In this case the rule states that the event should be repeated on every weekday for the next twenty days. The values for the recurrence rule as well as for the duration have to be given in an RFC 5545 compliant format.

Alas, the RFC formats for the duration and the recurrence rule are less than obvious. Believe me: You do not want to know the detailed rules for the recurrence format. This part of the specification alone is eight pages long and Google's class EventRecurrence, representing and parsing those rules, is about 900 lines long. Explaining these formats is way beyond the scope of this blog post!

Reading, updating and deleting events

Getting more information about an event is pretty simple. You just need the Events.CONTENT_URI and the appropriate selection. If - for example - you know the event id you can access the event like this:


long selectedEventId = // the event-id;
String[] proj = 
      new String[]{
            Events._ID, 
            Events.DTSTART, 
            Events.DTEND, 
            Events.RRULE, 
            Events.TITLE};
Cursor cursor = 
      getContentResolver().
            query(
               Events.CONTENT_URI, 
               proj, 
               Events._ID + " = ? ", 
               new String[]{Long.toString(selectedEventId)}, 
               null);
if (cursor.moveToFirst()) {
   // read event data
}

Sometimes you want to find out more about events of the device owner. E.g. a concert planning app could have a look at the users calendar and check that she is free that evening. That's what the next code snippet does. It checks if any event instances exist between a starting point and an ending point in time.


long begin = // starting time in milliseconds
long end = // ending time in milliseconds
String[] proj = 
      new String[]{
            Instances._ID, 
            Instances.BEGIN, 
            Instances.END, 
            Instances.EVENT_ID};
Cursor cursor = 
      Instances.query(getContentResolver(), proj, begin, end);
if (cursor.getCount() > 0) {
   // deal with conflict
}

Updating and deleting events are pretty easy tasks to accomplish. To delete you simply need the id of the event you want to delete:


String[] selArgs = 
      new String[]{Long.toString(selectedEventId)};
int deleted = 
      getContentResolver().
            delete(
               Events.CONTENT_URI, 
               Events._ID + " =? ", 
               selArgs);

For updating you need a ContentValues object containing those elements that you want to change:


ContentValues values = new ContentValues();
values.put(Events.TITLE, "Some new title");
values.put(Events.EVENT_LOCATION, "A new location");
String[] selArgs = 
      new String[]{Long.toString(selectedEventId)};
int updated = 
      getContentResolver().
            update(
               Events.CONTENT_URI, 
               values, 
               Events._ID + " =? ", 
               selArgs);

Adding attendees and alarms

As I've mentioned above, inserting an event is not enough. You probably want to add attendees and alarms.

Both tables reference the events table by using the field EVENT_ID with the appropriate id.

As usual you need a ContentValues object to get this done:


// adding an attendee:
values.clear();
values.put(Attendees.EVENT_ID, eventId);
values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
values.put(Attendees.ATTENDEE_NAME, "Douglas Adams");
values.put(Attendees.ATTENDEE_EMAIL, "d.adams@zaphod-b.com");
getContentResolver().insert(Attendees.CONTENT_URI, values);
// adding a reminder:
values.clear();
values.put(Reminders.EVENT_ID, eventId);
values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
values.put(Reminders.MINUTES, 30);
getContentResolver().insert(Reminders.CONTENT_URI, values);

As explained in another post, you probably would want to use the class ContentProviderOperation to do a batch operation. But to keep the sample code as easy to read as possible, I have chosen the simpler approach shown above.

Other data

Reading, updating or deleting any of the other elements is pretty much like the sample code for events. Only the CONTENT_URI and the possible columns are different. Those are well documented in the CalendarContracts API.

If you want to insert data into other tables you have to be careful though: Only SyncAdapters are allowed to add records to Colors, SyncState or ExtendedProperties. If your ignore this restriction your code will result in an IllegalArgumentException at runtime.

Furthermore you are not allowed to manipulate data of the instances table - even if you are a sync adapter. Android will take care of instances on its own whenever an event gets added, deleted or modified. The only thing you are allowed to do with instances is to query them.

What about older Android devices?

As mentioned, the Calendar app and the content provider have been introduced with Ice Cream Sandwich. Even though the market share of 4.x devices increases steadily, most devices out there are still 2.x devices. Does anything comparable exist for them as well?

Well, kind of. Of course nothing official existed prior to ICS and the emulator images for older devices didn't and still don't have a calendar app. But nearly every device has. Even the sources existed - though marked with the @hide annotation, so that you can't use the classes (e.g. the final static fields for the columns) in your code.

This is a sign that Google regarded the code as yet not ready for prime time. Well, rightly so, since it is still buggy in places - though mostly in the app, as I will explain in my next post.

If you take a look at the Calendar content provider of Android 2.3.7 at grepcode you can see that the data model is pretty similar to the one of the current provider. If you diff older code and newer code you will notice how close both are. Most changes deal with the new table colors. Obviously any code that relies on this table is doomed to fail on older devices.

So can you simply act as if the provider had been around for ages? Well, no. You can't! The code used in this tutorial would result in NoClassDefFoundErrors.

But, and this is the good news, the old provider can still be used for most tasks without too much changes. For this to work you mostly have to exchange all constants with their actual values.

Be very careful if you do so! You have to test vigorously and you risk running into problems if the values (column names, content uris and so on) should ever change. And be prepared for content providers that do not match this data model - you simply cannot rely on it. Try to isolate the critical parts as much as possible.

If you want to know more about accessing calendars prior to ICS, you should read the blog post of Jim Blackler about the old internal calendar database.

Wrapping up

In this part of the tutorial I have shown you how to use Android’s CalendarContract content provider.

Using the content provider you can add events or get information about the user's events and you can also check for conflicting dates.

To make full use of the content provider you have to be a sync adapter. But using a device local calendar account you can also deal with calendars in many ways.

In the next part of this tutorial I am going to cover the Intents that are provided by the Calendar app. For some use cases they might be the better choice - but of course you have much more flexibility by using the content provider.

Wolfram Rittmeyer lives in Germany and has been developing with Java for many years.

He has been interested in Android for quite a while and has been blogging about all kind of topics around Android.

You can find him on Google+ and Twitter.