summaryrefslogtreecommitdiffstats
path: root/core/java/android/pim
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
commit9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch)
treed88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/pim
parentd83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff)
downloadframeworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/pim')
-rw-r--r--core/java/android/pim/ContactsAsyncHelper.java336
-rw-r--r--core/java/android/pim/DateException.java26
-rw-r--r--core/java/android/pim/EventRecurrence.java421
-rw-r--r--core/java/android/pim/ICalendar.java644
-rw-r--r--core/java/android/pim/RecurrenceSet.java398
-rw-r--r--core/java/android/pim/package.html7
6 files changed, 1832 insertions, 0 deletions
diff --git a/core/java/android/pim/ContactsAsyncHelper.java b/core/java/android/pim/ContactsAsyncHelper.java
new file mode 100644
index 0000000..a21281e
--- /dev/null
+++ b/core/java/android/pim/ContactsAsyncHelper.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.pim;
+
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.Connection;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.Contacts;
+import android.provider.Contacts.People;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+
+import java.io.InputStream;
+
+/**
+ * Helper class for async access of images.
+ */
+public class ContactsAsyncHelper extends Handler {
+
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "ContactsAsyncHelper";
+
+ /**
+ * Interface for a WorkerHandler result return.
+ */
+ public interface OnImageLoadCompleteListener {
+ /**
+ * Called when the image load is complete.
+ *
+ * @param imagePresent true if an image was found
+ */
+ public void onImageLoadComplete(int token, Object cookie, ImageView iView,
+ boolean imagePresent);
+ }
+
+ // constants
+ private static final int EVENT_LOAD_IMAGE = 1;
+ private static final int DEFAULT_TOKEN = -1;
+
+ // static objects
+ private static Handler sThreadHandler;
+ private static ContactsAsyncHelper sInstance;
+
+ static {
+ sInstance = new ContactsAsyncHelper();
+ }
+
+ private static final class WorkerArgs {
+ public Context context;
+ public ImageView view;
+ public Uri uri;
+ public int defaultResource;
+ public Object result;
+ public Object cookie;
+ public OnImageLoadCompleteListener listener;
+ public CallerInfo info;
+ }
+
+ /**
+ * public inner class to help out the ContactsAsyncHelper callers
+ * with tracking the state of the CallerInfo Queries and image
+ * loading.
+ *
+ * Logic contained herein is used to remove the race conditions
+ * that exist as the CallerInfo queries run and mix with the image
+ * loads, which then mix with the Phone state changes.
+ */
+ public static class ImageTracker {
+
+ // Image display states
+ public static final int DISPLAY_UNDEFINED = 0;
+ public static final int DISPLAY_IMAGE = -1;
+ public static final int DISPLAY_DEFAULT = -2;
+
+ // State of the image on the imageview.
+ private CallerInfo mCurrentCallerInfo;
+ private int displayMode;
+
+ public ImageTracker() {
+ mCurrentCallerInfo = null;
+ displayMode = DISPLAY_UNDEFINED;
+ }
+
+ /**
+ * Used to see if the requested call / connection has a
+ * different caller attached to it than the one we currently
+ * have in the CallCard.
+ */
+ public boolean isDifferentImageRequest(CallerInfo ci) {
+ // note, since the connections are around for the lifetime of the
+ // call, and the CallerInfo-related items as well, we can
+ // definitely use a simple != comparison.
+ return (mCurrentCallerInfo != ci);
+ }
+
+ public boolean isDifferentImageRequest(Connection connection) {
+ // if the connection does not exist, see if the
+ // mCurrentCallerInfo is also null to match.
+ if (connection == null) {
+ if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null");
+ return (mCurrentCallerInfo != null);
+ }
+ Object o = connection.getUserData();
+
+ // if the call does NOT have a callerInfo attached
+ // then it is ok to query.
+ boolean runQuery = true;
+ if (o instanceof CallerInfo) {
+ runQuery = isDifferentImageRequest((CallerInfo) o);
+ }
+ return runQuery;
+ }
+
+ /**
+ * Simple setter for the CallerInfo object.
+ */
+ public void setPhotoRequest(CallerInfo ci) {
+ mCurrentCallerInfo = ci;
+ }
+
+ /**
+ * Convenience method used to retrieve the URI
+ * representing the Photo file recorded in the attached
+ * CallerInfo Object.
+ */
+ public Uri getPhotoUri() {
+ if (mCurrentCallerInfo != null) {
+ return ContentUris.withAppendedId(People.CONTENT_URI,
+ mCurrentCallerInfo.person_id);
+ }
+ return null;
+ }
+
+ /**
+ * Simple setter for the Photo state.
+ */
+ public void setPhotoState(int state) {
+ displayMode = state;
+ }
+
+ /**
+ * Simple getter for the Photo state.
+ */
+ public int getPhotoState() {
+ return displayMode;
+ }
+ }
+
+ /**
+ * Thread worker class that handles the task of opening the stream and loading
+ * the images.
+ */
+ private class WorkerHandler extends Handler {
+ public WorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ InputStream inputStream = Contacts.People.openContactPhotoInputStream(
+ args.context.getContentResolver(), args.uri);
+ if (inputStream != null) {
+ args.result = Drawable.createFromStream(inputStream, args.uri.toString());
+
+ if (DBG) Log.d(LOG_TAG, "Loading image: " + msg.arg1 +
+ " token: " + msg.what + " image URI: " + args.uri);
+ } else {
+ args.result = null;
+ if (DBG) Log.d(LOG_TAG, "Problem with image: " + msg.arg1 +
+ " token: " + msg.what + " image URI: " + args.uri +
+ ", using default image.");
+ }
+ break;
+ default:
+ }
+
+ // send the reply to the enclosing class.
+ Message reply = ContactsAsyncHelper.this.obtainMessage(msg.what);
+ reply.arg1 = msg.arg1;
+ reply.obj = msg.obj;
+ reply.sendToTarget();
+ }
+ }
+
+ /**
+ * Private constructor for static class
+ */
+ private ContactsAsyncHelper() {
+ HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
+ thread.start();
+ sThreadHandler = new WorkerHandler(thread.getLooper());
+ }
+
+ /**
+ * Convenience method for calls that do not want to deal with listeners and tokens.
+ */
+ public static final void updateImageViewWithContactPhotoAsync(Context context,
+ ImageView imageView, Uri person, int placeholderImageResource) {
+ // Added additional Cookie field in the callee.
+ updateImageViewWithContactPhotoAsync (null, DEFAULT_TOKEN, null, null, context,
+ imageView, person, placeholderImageResource);
+ }
+
+ /**
+ * Convenience method for calls that do not want to deal with listeners and tokens, but have
+ * a CallerInfo object to cache the image to.
+ */
+ public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, Context context,
+ ImageView imageView, Uri person, int placeholderImageResource) {
+ // Added additional Cookie field in the callee.
+ updateImageViewWithContactPhotoAsync (info, DEFAULT_TOKEN, null, null, context,
+ imageView, person, placeholderImageResource);
+ }
+
+
+ /**
+ * Start an image load, attach the result to the specified CallerInfo object.
+ * Note, when the query is started, we make the ImageView INVISIBLE if the
+ * placeholderImageResource value is -1. When we're given a valid (!= -1)
+ * placeholderImageResource value, we make sure the image is visible.
+ */
+ public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, int token,
+ OnImageLoadCompleteListener listener, Object cookie, Context context,
+ ImageView imageView, Uri person, int placeholderImageResource) {
+
+ // in case the source caller info is null, the URI will be null as well.
+ // just update using the placeholder image in this case.
+ if (person == null) {
+ if (DBG) Log.d(LOG_TAG, "target image is null, just display placeholder.");
+ imageView.setVisibility(View.VISIBLE);
+ imageView.setImageResource(placeholderImageResource);
+ return;
+ }
+
+ // Added additional Cookie field in the callee to handle arguments
+ // sent to the callback function.
+
+ // setup arguments
+ WorkerArgs args = new WorkerArgs();
+ args.cookie = cookie;
+ args.context = context;
+ args.view = imageView;
+ args.uri = person;
+ args.defaultResource = placeholderImageResource;
+ args.listener = listener;
+ args.info = info;
+
+ // setup message arguments
+ Message msg = sThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_LOAD_IMAGE;
+ msg.obj = args;
+
+ if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri +
+ ", displaying default image for now.");
+
+ // set the default image first, when the query is complete, we will
+ // replace the image with the correct one.
+ if (placeholderImageResource != -1) {
+ imageView.setVisibility(View.VISIBLE);
+ imageView.setImageResource(placeholderImageResource);
+ } else {
+ imageView.setVisibility(View.INVISIBLE);
+ }
+
+ // notify the thread to begin working
+ sThreadHandler.sendMessage(msg);
+ }
+
+ /**
+ * Called when loading is done.
+ */
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ boolean imagePresent = false;
+
+ // if the image has been loaded then display it, otherwise set default.
+ // in either case, make sure the image is visible.
+ if (args.result != null) {
+ args.view.setVisibility(View.VISIBLE);
+ args.view.setImageDrawable((Drawable) args.result);
+ // make sure the cached photo data is updated.
+ if (args.info != null) {
+ args.info.cachedPhoto = (Drawable) args.result;
+ }
+ imagePresent = true;
+ } else if (args.defaultResource != -1) {
+ args.view.setVisibility(View.VISIBLE);
+ args.view.setImageResource(args.defaultResource);
+ }
+
+ // Note that the data is cached.
+ if (args.info != null) {
+ args.info.isCachedPhotoCurrent = true;
+ }
+
+ // notify the listener if it is there.
+ if (args.listener != null) {
+ if (DBG) Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() +
+ " image: " + args.uri + " completed");
+ args.listener.onImageLoadComplete(msg.what, args.cookie, args.view,
+ imagePresent);
+ }
+ break;
+ default:
+ }
+ }
+}
diff --git a/core/java/android/pim/DateException.java b/core/java/android/pim/DateException.java
new file mode 100644
index 0000000..90bfe7f
--- /dev/null
+++ b/core/java/android/pim/DateException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.pim;
+
+public class DateException extends Exception
+{
+ public DateException(String message)
+ {
+ super(message);
+ }
+}
+
diff --git a/core/java/android/pim/EventRecurrence.java b/core/java/android/pim/EventRecurrence.java
new file mode 100644
index 0000000..edf69ee
--- /dev/null
+++ b/core/java/android/pim/EventRecurrence.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.pim;
+
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.text.format.Time;
+
+import java.util.Calendar;
+
+public class EventRecurrence
+{
+ /**
+ * Thrown when a recurrence string provided can not be parsed according
+ * to RFC2445.
+ */
+ public static class InvalidFormatException extends RuntimeException
+ {
+ InvalidFormatException(String s) {
+ super(s);
+ }
+ }
+
+ public EventRecurrence()
+ {
+ wkst = MO;
+ }
+
+ /**
+ * Parse an iCalendar/RFC2445 recur type according to Section 4.3.10.
+ */
+ public native void parse(String recur);
+
+ public void setStartDate(Time date) {
+ startDate = date;
+ }
+
+ public static final int SECONDLY = 1;
+ public static final int MINUTELY = 2;
+ public static final int HOURLY = 3;
+ public static final int DAILY = 4;
+ public static final int WEEKLY = 5;
+ public static final int MONTHLY = 6;
+ public static final int YEARLY = 7;
+
+ public static final int SU = 0x00010000;
+ public static final int MO = 0x00020000;
+ public static final int TU = 0x00040000;
+ public static final int WE = 0x00080000;
+ public static final int TH = 0x00100000;
+ public static final int FR = 0x00200000;
+ public static final int SA = 0x00400000;
+
+ public Time startDate;
+ public int freq;
+ public String until;
+ public int count;
+ public int interval;
+ public int wkst;
+
+ public int[] bysecond;
+ public int bysecondCount;
+ public int[] byminute;
+ public int byminuteCount;
+ public int[] byhour;
+ public int byhourCount;
+ public int[] byday;
+ public int[] bydayNum;
+ public int bydayCount;
+ public int[] bymonthday;
+ public int bymonthdayCount;
+ public int[] byyearday;
+ public int byyeardayCount;
+ public int[] byweekno;
+ public int byweeknoCount;
+ public int[] bymonth;
+ public int bymonthCount;
+ public int[] bysetpos;
+ public int bysetposCount;
+
+ /**
+ * Converts one of the Calendar.SUNDAY constants to the SU, MO, etc.
+ * constants. btw, I think we should switch to those here too, to
+ * get rid of this function, if possible.
+ */
+ public static int calendarDay2Day(int day)
+ {
+ switch (day)
+ {
+ case Calendar.SUNDAY:
+ return SU;
+ case Calendar.MONDAY:
+ return MO;
+ case Calendar.TUESDAY:
+ return TU;
+ case Calendar.WEDNESDAY:
+ return WE;
+ case Calendar.THURSDAY:
+ return TH;
+ case Calendar.FRIDAY:
+ return FR;
+ case Calendar.SATURDAY:
+ return SA;
+ default:
+ throw new RuntimeException("bad day of week: " + day);
+ }
+ }
+
+ public static int timeDay2Day(int day)
+ {
+ switch (day)
+ {
+ case Time.SUNDAY:
+ return SU;
+ case Time.MONDAY:
+ return MO;
+ case Time.TUESDAY:
+ return TU;
+ case Time.WEDNESDAY:
+ return WE;
+ case Time.THURSDAY:
+ return TH;
+ case Time.FRIDAY:
+ return FR;
+ case Time.SATURDAY:
+ return SA;
+ default:
+ throw new RuntimeException("bad day of week: " + day);
+ }
+ }
+ public static int day2TimeDay(int day)
+ {
+ switch (day)
+ {
+ case SU:
+ return Time.SUNDAY;
+ case MO:
+ return Time.MONDAY;
+ case TU:
+ return Time.TUESDAY;
+ case WE:
+ return Time.WEDNESDAY;
+ case TH:
+ return Time.THURSDAY;
+ case FR:
+ return Time.FRIDAY;
+ case SA:
+ return Time.SATURDAY;
+ default:
+ throw new RuntimeException("bad day of week: " + day);
+ }
+ }
+
+ /**
+ * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY
+ * constants. btw, I think we should switch to those here too, to
+ * get rid of this function, if possible.
+ */
+ public static int day2CalendarDay(int day)
+ {
+ switch (day)
+ {
+ case SU:
+ return Calendar.SUNDAY;
+ case MO:
+ return Calendar.MONDAY;
+ case TU:
+ return Calendar.TUESDAY;
+ case WE:
+ return Calendar.WEDNESDAY;
+ case TH:
+ return Calendar.THURSDAY;
+ case FR:
+ return Calendar.FRIDAY;
+ case SA:
+ return Calendar.SATURDAY;
+ default:
+ throw new RuntimeException("bad day of week: " + day);
+ }
+ }
+
+ /**
+ * Converts one of the internal day constants (SU, MO, etc.) to the
+ * two-letter string representing that constant.
+ *
+ * @throws IllegalArgumentException Thrown if the day argument is not one of
+ * the defined day constants.
+ *
+ * @param day one the internal constants SU, MO, etc.
+ * @return the two-letter string for the day ("SU", "MO", etc.)
+ */
+ private static String day2String(int day) {
+ switch (day) {
+ case SU:
+ return "SU";
+ case MO:
+ return "MO";
+ case TU:
+ return "TU";
+ case WE:
+ return "WE";
+ case TH:
+ return "TH";
+ case FR:
+ return "FR";
+ case SA:
+ return "SA";
+ default:
+ throw new IllegalArgumentException("bad day argument: " + day);
+ }
+ }
+
+ private static void appendNumbers(StringBuilder s, String label,
+ int count, int[] values)
+ {
+ if (count > 0) {
+ s.append(label);
+ count--;
+ for (int i=0; i<count; i++) {
+ s.append(values[i]);
+ s.append(",");
+ }
+ s.append(values[count]);
+ }
+ }
+
+ private void appendByDay(StringBuilder s, int i)
+ {
+ int n = this.bydayNum[i];
+ if (n != 0) {
+ s.append(n);
+ }
+
+ String str = day2String(this.byday[i]);
+ s.append(str);
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder s = new StringBuilder();
+
+ s.append("FREQ=");
+ switch (this.freq)
+ {
+ case SECONDLY:
+ s.append("SECONDLY");
+ break;
+ case MINUTELY:
+ s.append("MINUTELY");
+ break;
+ case HOURLY:
+ s.append("HOURLY");
+ break;
+ case DAILY:
+ s.append("DAILY");
+ break;
+ case WEEKLY:
+ s.append("WEEKLY");
+ break;
+ case MONTHLY:
+ s.append("MONTHLY");
+ break;
+ case YEARLY:
+ s.append("YEARLY");
+ break;
+ }
+
+ if (!TextUtils.isEmpty(this.until)) {
+ s.append(";UNTIL=");
+ s.append(until);
+ }
+
+ if (this.count != 0) {
+ s.append(";COUNT=");
+ s.append(this.count);
+ }
+
+ if (this.interval != 0) {
+ s.append(";INTERVAL=");
+ s.append(this.interval);
+ }
+
+ if (this.wkst != 0) {
+ s.append(";WKST=");
+ s.append(day2String(this.wkst));
+ }
+
+ appendNumbers(s, ";BYSECOND=", this.bysecondCount, this.bysecond);
+ appendNumbers(s, ";BYMINUTE=", this.byminuteCount, this.byminute);
+ appendNumbers(s, ";BYSECOND=", this.byhourCount, this.byhour);
+
+ // day
+ int count = this.bydayCount;
+ if (count > 0) {
+ s.append(";BYDAY=");
+ count--;
+ for (int i=0; i<count; i++) {
+ appendByDay(s, i);
+ s.append(",");
+ }
+ appendByDay(s, count);
+ }
+
+ appendNumbers(s, ";BYMONTHDAY=", this.bymonthdayCount, this.bymonthday);
+ appendNumbers(s, ";BYYEARDAY=", this.byyeardayCount, this.byyearday);
+ appendNumbers(s, ";BYWEEKNO=", this.byweeknoCount, this.byweekno);
+ appendNumbers(s, ";BYMONTH=", this.bymonthCount, this.bymonth);
+ appendNumbers(s, ";BYSETPOS=", this.bysetposCount, this.bysetpos);
+
+ return s.toString();
+ }
+
+ public String getRepeatString() {
+ Resources r = Resources.getSystem();
+
+ // TODO Implement "Until" portion of string, as well as custom settings
+ switch (this.freq) {
+ case DAILY:
+ return r.getString(com.android.internal.R.string.daily);
+ case WEEKLY: {
+ if (repeatsOnEveryWeekDay()) {
+ return r.getString(com.android.internal.R.string.every_weekday);
+ } else {
+ String format = r.getString(com.android.internal.R.string.weekly);
+ StringBuilder days = new StringBuilder();
+
+ // Do one less iteration in the loop so the last element is added out of the
+ // loop. This is done so the comma is not placed after the last item.
+ int count = this.bydayCount - 1;
+ if (count >= 0) {
+ for (int i = 0 ; i < count ; i++) {
+ days.append(dayToString(r, this.byday[i]));
+ days.append(",");
+ }
+ days.append(dayToString(r, this.byday[count]));
+
+ return String.format(format, days.toString());
+ }
+
+ // There is no "BYDAY" specifier, so use the day of the
+ // first event. For this to work, the setStartDate()
+ // method must have been used by the caller to set the
+ // date of the first event in the recurrence.
+ if (startDate == null) {
+ return null;
+ }
+
+ int day = timeDay2Day(startDate.weekDay);
+ return String.format(format, dayToString(r, day));
+ }
+ }
+ case MONTHLY: {
+ return r.getString(com.android.internal.R.string.monthly);
+ }
+ case YEARLY:
+ return r.getString(com.android.internal.R.string.yearly);
+ }
+
+ return null;
+ }
+
+ public boolean repeatsOnEveryWeekDay() {
+ if (this.freq != WEEKLY) {
+ return false;
+ }
+
+ int count = this.bydayCount;
+ if (count != 5) {
+ return false;
+ }
+
+ for (int i = 0 ; i < count ; i++) {
+ int day = byday[i];
+ if (day == SU || day == SA) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public boolean repeatsMonthlyOnDayCount() {
+ if (this.freq != MONTHLY) {
+ return false;
+ }
+
+ if (bydayCount != 1 || bymonthdayCount != 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private String dayToString(Resources r, int day) {
+ switch (day) {
+ case SU: return r.getString(com.android.internal.R.string.sunday);
+ case MO: return r.getString(com.android.internal.R.string.monday);
+ case TU: return r.getString(com.android.internal.R.string.tuesday);
+ case WE: return r.getString(com.android.internal.R.string.wednesday);
+ case TH: return r.getString(com.android.internal.R.string.thursday);
+ case FR: return r.getString(com.android.internal.R.string.friday);
+ case SA: return r.getString(com.android.internal.R.string.saturday);
+ default: throw new IllegalArgumentException("bad day argument: " + day);
+ }
+ }
+}
diff --git a/core/java/android/pim/ICalendar.java b/core/java/android/pim/ICalendar.java
new file mode 100644
index 0000000..cc0f45e
--- /dev/null
+++ b/core/java/android/pim/ICalendar.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.pim;
+
+import android.util.Log;
+import android.util.Config;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.ArrayList;
+
+/**
+ * Parses RFC 2445 iCalendar objects.
+ */
+public class ICalendar {
+
+ private static final String TAG = "Sync";
+
+ // TODO: keep track of VEVENT, VTODO, VJOURNAL, VFREEBUSY, VTIMEZONE, VALARM
+ // components, by type field or by subclass? subclass would allow us to
+ // enforce grammars.
+
+ /**
+ * Exception thrown when an iCalendar object has invalid syntax.
+ */
+ public static class FormatException extends Exception {
+ public FormatException() {
+ super();
+ }
+
+ public FormatException(String msg) {
+ super(msg);
+ }
+
+ public FormatException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+ }
+
+ /**
+ * A component within an iCalendar (VEVENT, VTODO, VJOURNAL, VFEEBUSY,
+ * VTIMEZONE, VALARM).
+ */
+ public static class Component {
+
+ // components
+ private static final String BEGIN = "BEGIN";
+ private static final String END = "END";
+ private static final String NEWLINE = "\n";
+ public static final String VCALENDAR = "VCALENDAR";
+ public static final String VEVENT = "VEVENT";
+ public static final String VTODO = "VTODO";
+ public static final String VJOURNAL = "VJOURNAL";
+ public static final String VFREEBUSY = "VFREEBUSY";
+ public static final String VTIMEZONE = "VTIMEZONE";
+ public static final String VALARM = "VALARM";
+
+ private final String mName;
+ private final Component mParent; // see if we can get rid of this
+ private LinkedList<Component> mChildren = null;
+ private final LinkedHashMap<String, ArrayList<Property>> mPropsMap =
+ new LinkedHashMap<String, ArrayList<Property>>();
+
+ /**
+ * Creates a new component with the provided name.
+ * @param name The name of the component.
+ */
+ public Component(String name, Component parent) {
+ mName = name;
+ mParent = parent;
+ }
+
+ /**
+ * Returns the name of the component.
+ * @return The name of the component.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the parent of this component.
+ * @return The parent of this component.
+ */
+ public Component getParent() {
+ return mParent;
+ }
+
+ /**
+ * Helper that lazily gets/creates the list of children.
+ * @return The list of children.
+ */
+ protected LinkedList<Component> getOrCreateChildren() {
+ if (mChildren == null) {
+ mChildren = new LinkedList<Component>();
+ }
+ return mChildren;
+ }
+
+ /**
+ * Adds a child component to this component.
+ * @param child The child component.
+ */
+ public void addChild(Component child) {
+ getOrCreateChildren().add(child);
+ }
+
+ /**
+ * Returns a list of the Component children of this component. May be
+ * null, if there are no children.
+ *
+ * @return A list of the children.
+ */
+ public List<Component> getComponents() {
+ return mChildren;
+ }
+
+ /**
+ * Adds a Property to this component.
+ * @param prop
+ */
+ public void addProperty(Property prop) {
+ String name= prop.getName();
+ ArrayList<Property> props = mPropsMap.get(name);
+ if (props == null) {
+ props = new ArrayList<Property>();
+ mPropsMap.put(name, props);
+ }
+ props.add(prop);
+ }
+
+ /**
+ * Returns a set of the property names within this component.
+ * @return A set of property names within this component.
+ */
+ public Set<String> getPropertyNames() {
+ return mPropsMap.keySet();
+ }
+
+ /**
+ * Returns a list of properties with the specified name. Returns null
+ * if there are no such properties.
+ * @param name The name of the property that should be returned.
+ * @return A list of properties with the requested name.
+ */
+ public List<Property> getProperties(String name) {
+ return mPropsMap.get(name);
+ }
+
+ /**
+ * Returns the first property with the specified name. Returns null
+ * if there is no such property.
+ * @param name The name of the property that should be returned.
+ * @return The first property with the specified name.
+ */
+ public Property getFirstProperty(String name) {
+ List<Property> props = mPropsMap.get(name);
+ if (props == null || props.size() == 0) {
+ return null;
+ }
+ return props.get(0);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(sb);
+ sb.append(NEWLINE);
+ return sb.toString();
+ }
+
+ /**
+ * Helper method that appends this component to a StringBuilder. The
+ * caller is responsible for appending a newline at the end of the
+ * component.
+ */
+ public void toString(StringBuilder sb) {
+ sb.append(BEGIN);
+ sb.append(":");
+ sb.append(mName);
+ sb.append(NEWLINE);
+
+ // append the properties
+ for (String propertyName : getPropertyNames()) {
+ for (Property property : getProperties(propertyName)) {
+ property.toString(sb);
+ sb.append(NEWLINE);
+ }
+ }
+
+ // append the sub-components
+ if (mChildren != null) {
+ for (Component component : mChildren) {
+ component.toString(sb);
+ sb.append(NEWLINE);
+ }
+ }
+
+ sb.append(END);
+ sb.append(":");
+ sb.append(mName);
+ }
+ }
+
+ /**
+ * A property within an iCalendar component (e.g., DTSTART, DTEND, etc.,
+ * within a VEVENT).
+ */
+ public static class Property {
+ // properties
+ // TODO: do we want to list these here? the complete list is long.
+ public static final String DTSTART = "DTSTART";
+ public static final String DTEND = "DTEND";
+ public static final String DURATION = "DURATION";
+ public static final String RRULE = "RRULE";
+ public static final String RDATE = "RDATE";
+ public static final String EXRULE = "EXRULE";
+ public static final String EXDATE = "EXDATE";
+ // ... need to add more.
+
+ private final String mName;
+ private LinkedHashMap<String, ArrayList<Parameter>> mParamsMap =
+ new LinkedHashMap<String, ArrayList<Parameter>>();
+ private String mValue; // TODO: make this final?
+
+ /**
+ * Creates a new property with the provided name.
+ * @param name The name of the property.
+ */
+ public Property(String name) {
+ mName = name;
+ }
+
+ /**
+ * Creates a new property with the provided name and value.
+ * @param name The name of the property.
+ * @param value The value of the property.
+ */
+ public Property(String name, String value) {
+ mName = name;
+ mValue = value;
+ }
+
+ /**
+ * Returns the name of the property.
+ * @return The name of the property.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the value of this property.
+ * @return The value of this property.
+ */
+ public String getValue() {
+ return mValue;
+ }
+
+ /**
+ * Sets the value of this property.
+ * @param value The desired value for this property.
+ */
+ public void setValue(String value) {
+ mValue = value;
+ }
+
+ /**
+ * Adds a {@link Parameter} to this property.
+ * @param param The parameter that should be added.
+ */
+ public void addParameter(Parameter param) {
+ ArrayList<Parameter> params = mParamsMap.get(param.name);
+ if (params == null) {
+ params = new ArrayList<Parameter>();
+ mParamsMap.put(param.name, params);
+ }
+ params.add(param);
+ }
+
+ /**
+ * Returns the set of parameter names for this property.
+ * @return The set of parameter names for this property.
+ */
+ public Set<String> getParameterNames() {
+ return mParamsMap.keySet();
+ }
+
+ /**
+ * Returns the list of parameters with the specified name. May return
+ * null if there are no such parameters.
+ * @param name The name of the parameters that should be returned.
+ * @return The list of parameters with the specified name.
+ */
+ public List<Parameter> getParameters(String name) {
+ return mParamsMap.get(name);
+ }
+
+ /**
+ * Returns the first parameter with the specified name. May return
+ * nll if there is no such parameter.
+ * @param name The name of the parameter that should be returned.
+ * @return The first parameter with the specified name.
+ */
+ public Parameter getFirstParameter(String name) {
+ ArrayList<Parameter> params = mParamsMap.get(name);
+ if (params == null || params.size() == 0) {
+ return null;
+ }
+ return params.get(0);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(sb);
+ return sb.toString();
+ }
+
+ /**
+ * Helper method that appends this property to a StringBuilder. The
+ * caller is responsible for appending a newline after this property.
+ */
+ public void toString(StringBuilder sb) {
+ sb.append(mName);
+ Set<String> parameterNames = getParameterNames();
+ for (String parameterName : parameterNames) {
+ for (Parameter param : getParameters(parameterName)) {
+ sb.append(";");
+ param.toString(sb);
+ }
+ }
+ sb.append(":");
+ sb.append(mValue);
+ }
+ }
+
+ /**
+ * A parameter defined for an iCalendar property.
+ */
+ // TODO: make this a proper class rather than a struct?
+ public static class Parameter {
+ public String name;
+ public String value;
+
+ /**
+ * Creates a new empty parameter.
+ */
+ public Parameter() {
+ }
+
+ /**
+ * Creates a new parameter with the specified name and value.
+ * @param name The name of the parameter.
+ * @param value The value of the parameter.
+ */
+ public Parameter(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(sb);
+ return sb.toString();
+ }
+
+ /**
+ * Helper method that appends this parameter to a StringBuilder.
+ */
+ public void toString(StringBuilder sb) {
+ sb.append(name);
+ sb.append("=");
+ sb.append(value);
+ }
+ }
+
+ private static final class ParserState {
+ // public int lineNumber = 0;
+ public String line; // TODO: just point to original text
+ public int index;
+ }
+
+ // use factory method
+ private ICalendar() {
+ }
+
+ // TODO: get rid of this -- handle all of the parsing in one pass through
+ // the text.
+ private static String normalizeText(String text) {
+ // it's supposed to be \r\n, but not everyone does that
+ text = text.replaceAll("\r\n", "\n");
+ text = text.replaceAll("\r", "\n");
+
+ // we deal with line folding, by replacing all "\n " strings
+ // with nothing. The RFC specifies "\r\n " to be folded, but
+ // we handle "\n " and "\r " too because we can get those.
+ text = text.replaceAll("\n ", "");
+
+ return text;
+ }
+
+ /**
+ * Parses text into an iCalendar component. Parses into the provided
+ * component, if not null, or parses into a new component. In the latter
+ * case, expects a BEGIN as the first line. Returns the provided or newly
+ * created top-level component.
+ */
+ // TODO: use an index into the text, so we can make this a recursive
+ // function?
+ private static Component parseComponentImpl(Component component,
+ String text)
+ throws FormatException {
+ Component current = component;
+ ParserState state = new ParserState();
+ state.index = 0;
+
+ // split into lines
+ String[] lines = text.split("\n");
+
+ // each line is of the format:
+ // name *(";" param) ":" value
+ for (String line : lines) {
+ try {
+ current = parseLine(line, state, current);
+ // if the provided component was null, we will return the root
+ // NOTE: in this case, if the first line is not a BEGIN, a
+ // FormatException will get thrown.
+ if (component == null) {
+ component = current;
+ }
+ } catch (FormatException fe) {
+ if (Config.LOGV) {
+ Log.v(TAG, "Cannot parse " + line, fe);
+ }
+ // for now, we ignore the parse error. Google Calendar seems
+ // to be emitting some misformatted iCalendar objects.
+ }
+ continue;
+ }
+ return component;
+ }
+
+ /**
+ * Parses a line into the provided component. Creates a new component if
+ * the line is a BEGIN, adding the newly created component to the provided
+ * parent. Returns whatever component is the current one (to which new
+ * properties will be added) in the parse.
+ */
+ private static Component parseLine(String line, ParserState state,
+ Component component)
+ throws FormatException {
+ state.line = line;
+ int len = state.line.length();
+
+ // grab the name
+ char c = 0;
+ for (state.index = 0; state.index < len; ++state.index) {
+ c = line.charAt(state.index);
+ if (c == ';' || c == ':') {
+ break;
+ }
+ }
+ String name = line.substring(0, state.index);
+
+ if (component == null) {
+ if (!Component.BEGIN.equals(name)) {
+ throw new FormatException("Expected BEGIN");
+ }
+ }
+
+ Property property;
+ if (Component.BEGIN.equals(name)) {
+ // start a new component
+ String componentName = extractValue(state);
+ Component child = new Component(componentName, component);
+ if (component != null) {
+ component.addChild(child);
+ }
+ return child;
+ } else if (Component.END.equals(name)) {
+ // finish the current component
+ String componentName = extractValue(state);
+ if (component == null ||
+ !componentName.equals(component.getName())) {
+ throw new FormatException("Unexpected END " + componentName);
+ }
+ return component.getParent();
+ } else {
+ property = new Property(name);
+ }
+
+ if (c == ';') {
+ Parameter parameter = null;
+ while ((parameter = extractParameter(state)) != null) {
+ property.addParameter(parameter);
+ }
+ }
+ String value = extractValue(state);
+ property.setValue(value);
+ component.addProperty(property);
+ return component;
+ }
+
+ /**
+ * Extracts the value ":..." on the current line. The first character must
+ * be a ':'.
+ */
+ private static String extractValue(ParserState state)
+ throws FormatException {
+ String line = state.line;
+ if (state.index >= line.length() || line.charAt(state.index) != ':') {
+ throw new FormatException("Expected ':' before end of line in "
+ + line);
+ }
+ String value = line.substring(state.index + 1);
+ state.index = line.length() - 1;
+ return value;
+ }
+
+ /**
+ * Extracts the next parameter from the line, if any. If there are no more
+ * parameters, returns null.
+ */
+ private static Parameter extractParameter(ParserState state)
+ throws FormatException {
+ String text = state.line;
+ int len = text.length();
+ Parameter parameter = null;
+ int startIndex = -1;
+ int equalIndex = -1;
+ while (state.index < len) {
+ char c = text.charAt(state.index);
+ if (c == ':') {
+ if (parameter != null) {
+ if (equalIndex == -1) {
+ throw new FormatException("Expected '=' within "
+ + "parameter in " + text);
+ }
+ parameter.value = text.substring(equalIndex + 1,
+ state.index);
+ }
+ return parameter; // may be null
+ } else if (c == ';') {
+ if (parameter != null) {
+ if (equalIndex == -1) {
+ throw new FormatException("Expected '=' within "
+ + "parameter in " + text);
+ }
+ parameter.value = text.substring(equalIndex + 1,
+ state.index);
+ return parameter;
+ } else {
+ parameter = new Parameter();
+ startIndex = state.index;
+ }
+ } else if (c == '=') {
+ equalIndex = state.index;
+ if ((parameter == null) || (startIndex == -1)) {
+ throw new FormatException("Expected ';' before '=' in "
+ + text);
+ }
+ parameter.name = text.substring(startIndex + 1, equalIndex);
+ }
+ ++state.index;
+ }
+ throw new FormatException("Expected ':' before end of line in " + text);
+ }
+
+ /**
+ * Parses the provided text into an iCalendar object. The top-level
+ * component must be of type VCALENDAR.
+ * @param text The text to be parsed.
+ * @return The top-level VCALENDAR component.
+ * @throws FormatException Thrown if the text could not be parsed into an
+ * iCalendar VCALENDAR object.
+ */
+ public static Component parseCalendar(String text) throws FormatException {
+ Component calendar = parseComponent(null, text);
+ if (calendar == null || !Component.VCALENDAR.equals(calendar.getName())) {
+ throw new FormatException("Expected " + Component.VCALENDAR);
+ }
+ return calendar;
+ }
+
+ /**
+ * Parses the provided text into an iCalendar event. The top-level
+ * component must be of type VEVENT.
+ * @param text The text to be parsed.
+ * @return The top-level VEVENT component.
+ * @throws FormatException Thrown if the text could not be parsed into an
+ * iCalendar VEVENT.
+ */
+ public static Component parseEvent(String text) throws FormatException {
+ Component event = parseComponent(null, text);
+ if (event == null || !Component.VEVENT.equals(event.getName())) {
+ throw new FormatException("Expected " + Component.VEVENT);
+ }
+ return event;
+ }
+
+ /**
+ * Parses the provided text into an iCalendar component.
+ * @param text The text to be parsed.
+ * @return The top-level component.
+ * @throws FormatException Thrown if the text could not be parsed into an
+ * iCalendar component.
+ */
+ public static Component parseComponent(String text) throws FormatException {
+ return parseComponent(null, text);
+ }
+
+ /**
+ * Parses the provided text, adding to the provided component.
+ * @param component The component to which the parsed iCalendar data should
+ * be added.
+ * @param text The text to be parsed.
+ * @return The top-level component.
+ * @throws FormatException Thrown if the text could not be parsed as an
+ * iCalendar object.
+ */
+ public static Component parseComponent(Component component, String text)
+ throws FormatException {
+ text = normalizeText(text);
+ return parseComponentImpl(component, text);
+ }
+}
diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java
new file mode 100644
index 0000000..1a287c8
--- /dev/null
+++ b/core/java/android/pim/RecurrenceSet.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.pim;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.Calendar;
+import android.text.TextUtils;
+import android.text.format.Time;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Basic information about a recurrence, following RFC 2445 Section 4.8.5.
+ * Contains the RRULEs, RDATE, EXRULEs, and EXDATE properties.
+ */
+public class RecurrenceSet {
+
+ private final static String TAG = "CalendarProvider";
+
+ private final static String RULE_SEPARATOR = "\n";
+
+ // TODO: make these final?
+ public EventRecurrence[] rrules = null;
+ public long[] rdates = null;
+ public EventRecurrence[] exrules = null;
+ public long[] exdates = null;
+
+ /**
+ * Creates a new RecurrenceSet from information stored in the
+ * events table in the CalendarProvider.
+ * @param values The values retrieved from the Events table.
+ */
+ public RecurrenceSet(ContentValues values) {
+ String rruleStr = values.getAsString(Calendar.Events.RRULE);
+ String rdateStr = values.getAsString(Calendar.Events.RDATE);
+ String exruleStr = values.getAsString(Calendar.Events.EXRULE);
+ String exdateStr = values.getAsString(Calendar.Events.EXDATE);
+ init(rruleStr, rdateStr, exruleStr, exdateStr);
+ }
+
+ /**
+ * Creates a new RecurrenceSet from information stored in a database
+ * {@link Cursor} pointing to the events table in the
+ * CalendarProvider. The cursor must contain the RRULE, RDATE, EXRULE,
+ * and EXDATE columns.
+ *
+ * @param cursor The cursor containing the RRULE, RDATE, EXRULE, and EXDATE
+ * columns.
+ */
+ public RecurrenceSet(Cursor cursor) {
+ int rruleColumn = cursor.getColumnIndex(Calendar.Events.RRULE);
+ int rdateColumn = cursor.getColumnIndex(Calendar.Events.RDATE);
+ int exruleColumn = cursor.getColumnIndex(Calendar.Events.EXRULE);
+ int exdateColumn = cursor.getColumnIndex(Calendar.Events.EXDATE);
+ String rruleStr = cursor.getString(rruleColumn);
+ String rdateStr = cursor.getString(rdateColumn);
+ String exruleStr = cursor.getString(exruleColumn);
+ String exdateStr = cursor.getString(exdateColumn);
+ init(rruleStr, rdateStr, exruleStr, exdateStr);
+ }
+
+ public RecurrenceSet(String rruleStr, String rdateStr,
+ String exruleStr, String exdateStr) {
+ init(rruleStr, rdateStr, exruleStr, exdateStr);
+ }
+
+ private void init(String rruleStr, String rdateStr,
+ String exruleStr, String exdateStr) {
+ if (!TextUtils.isEmpty(rruleStr) || !TextUtils.isEmpty(rdateStr)) {
+
+ if (!TextUtils.isEmpty(rruleStr)) {
+ String[] rruleStrs = rruleStr.split(RULE_SEPARATOR);
+ rrules = new EventRecurrence[rruleStrs.length];
+ for (int i = 0; i < rruleStrs.length; ++i) {
+ EventRecurrence rrule = new EventRecurrence();
+ rrule.parse(rruleStrs[i]);
+ rrules[i] = rrule;
+ }
+ }
+
+ if (!TextUtils.isEmpty(rdateStr)) {
+ rdates = parseRecurrenceDates(rdateStr);
+ }
+
+ if (!TextUtils.isEmpty(exruleStr)) {
+ String[] exruleStrs = exruleStr.split(RULE_SEPARATOR);
+ exrules = new EventRecurrence[exruleStrs.length];
+ for (int i = 0; i < exruleStrs.length; ++i) {
+ EventRecurrence exrule = new EventRecurrence();
+ exrule.parse(exruleStr);
+ exrules[i] = exrule;
+ }
+ }
+
+ if (!TextUtils.isEmpty(exdateStr)) {
+ exdates = parseRecurrenceDates(exdateStr);
+ }
+ }
+ }
+
+ /**
+ * Returns whether or not a recurrence is defined in this RecurrenceSet.
+ * @return Whether or not a recurrence is defined in this RecurrenceSet.
+ */
+ public boolean hasRecurrence() {
+ return (rrules != null || rdates != null);
+ }
+
+ /**
+ * Parses the provided RDATE or EXDATE string into an array of longs
+ * representing each date/time in the recurrence.
+ * @param recurrence The recurrence to be parsed.
+ * @return The list of date/times.
+ */
+ public static long[] parseRecurrenceDates(String recurrence) {
+ // TODO: use "local" time as the default. will need to handle times
+ // that end in "z" (UTC time) explicitly at that point.
+ String tz = Time.TIMEZONE_UTC;
+ int tzidx = recurrence.indexOf(";");
+ if (tzidx != -1) {
+ tz = recurrence.substring(0, tzidx);
+ recurrence = recurrence.substring(tzidx + 1);
+ }
+ Time time = new Time(tz);
+ String[] rawDates = recurrence.split(",");
+ int n = rawDates.length;
+ long[] dates = new long[n];
+ for (int i = 0; i<n; ++i) {
+ // The timezone is updated to UTC if the time string specified 'Z'.
+ time.parse(rawDates[i]);
+ dates[i] = time.toMillis(false /* use isDst */);
+ time.timezone = tz;
+ }
+ return dates;
+ }
+
+ /**
+ * Populates the database map of values with the appropriate RRULE, RDATE,
+ * EXRULE, and EXDATE values extracted from the parsed iCalendar component.
+ * @param component The iCalendar component containing the desired
+ * recurrence specification.
+ * @param values The db values that should be updated.
+ * @return true if the component contained the necessary information
+ * to specify a recurrence. The required fields are DTSTART,
+ * one of DTEND/DURATION, and one of RRULE/RDATE. Returns false if
+ * there was an error, including if the date is out of range.
+ */
+ public static boolean populateContentValues(ICalendar.Component component,
+ ContentValues values) {
+ ICalendar.Property dtstartProperty =
+ component.getFirstProperty("DTSTART");
+ String dtstart = dtstartProperty.getValue();
+ ICalendar.Parameter tzidParam =
+ dtstartProperty.getFirstParameter("TZID");
+ // NOTE: the timezone may be null, if this is a floating time.
+ String tzid = tzidParam == null ? null : tzidParam.value;
+ Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid);
+ boolean inUtc = start.parse(dtstart);
+ boolean allDay = start.allDay;
+
+ if (inUtc) {
+ tzid = Time.TIMEZONE_UTC;
+ }
+
+ String duration = computeDuration(start, component);
+ String rrule = flattenProperties(component, "RRULE");
+ String rdate = extractDates(component.getFirstProperty("RDATE"));
+ String exrule = flattenProperties(component, "EXRULE");
+ String exdate = extractDates(component.getFirstProperty("EXDATE"));
+
+ if ((TextUtils.isEmpty(dtstart))||
+ (TextUtils.isEmpty(duration))||
+ ((TextUtils.isEmpty(rrule))&&
+ (TextUtils.isEmpty(rdate)))) {
+ if (Config.LOGD) {
+ Log.d(TAG, "Recurrence missing DTSTART, DTEND/DURATION, "
+ + "or RRULE/RDATE: "
+ + component.toString());
+ }
+ return false;
+ }
+
+ if (allDay) {
+ // TODO: also change tzid to be UTC? that would be consistent, but
+ // that would not reflect the original timezone value back to the
+ // server.
+ start.timezone = Time.TIMEZONE_UTC;
+ }
+ long millis = start.toMillis(false /* use isDst */);
+ values.put(Calendar.Events.DTSTART, millis);
+ if (millis == -1) {
+ if (Config.LOGD) {
+ Log.d(TAG, "DTSTART is out of range: " + component.toString());
+ }
+ return false;
+ }
+
+ values.put(Calendar.Events.RRULE, rrule);
+ values.put(Calendar.Events.RDATE, rdate);
+ values.put(Calendar.Events.EXRULE, exrule);
+ values.put(Calendar.Events.EXDATE, exdate);
+ values.put(Calendar.Events.EVENT_TIMEZONE, tzid);
+ values.put(Calendar.Events.DURATION, duration);
+ values.put(Calendar.Events.ALL_DAY, allDay ? 1 : 0);
+ return true;
+ }
+
+ public static boolean populateComponent(Cursor cursor,
+ ICalendar.Component component) {
+
+ int dtstartColumn = cursor.getColumnIndex(Calendar.Events.DTSTART);
+ int durationColumn = cursor.getColumnIndex(Calendar.Events.DURATION);
+ int tzidColumn = cursor.getColumnIndex(Calendar.Events.EVENT_TIMEZONE);
+ int rruleColumn = cursor.getColumnIndex(Calendar.Events.RRULE);
+ int rdateColumn = cursor.getColumnIndex(Calendar.Events.RDATE);
+ int exruleColumn = cursor.getColumnIndex(Calendar.Events.EXRULE);
+ int exdateColumn = cursor.getColumnIndex(Calendar.Events.EXDATE);
+ int allDayColumn = cursor.getColumnIndex(Calendar.Events.ALL_DAY);
+
+
+ long dtstart = -1;
+ if (!cursor.isNull(dtstartColumn)) {
+ dtstart = cursor.getLong(dtstartColumn);
+ }
+ String duration = cursor.getString(durationColumn);
+ String tzid = cursor.getString(tzidColumn);
+ String rruleStr = cursor.getString(rruleColumn);
+ String rdateStr = cursor.getString(rdateColumn);
+ String exruleStr = cursor.getString(exruleColumn);
+ String exdateStr = cursor.getString(exdateColumn);
+ boolean allDay = cursor.getInt(allDayColumn) == 1;
+
+ if ((dtstart == -1) ||
+ (TextUtils.isEmpty(duration))||
+ ((TextUtils.isEmpty(rruleStr))&&
+ (TextUtils.isEmpty(rdateStr)))) {
+ // no recurrence.
+ return false;
+ }
+
+ ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART");
+ Time dtstartTime = null;
+ if (!TextUtils.isEmpty(tzid)) {
+ if (!allDay) {
+ dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid));
+ }
+ dtstartTime = new Time(tzid);
+ } else {
+ // use the "floating" timezone
+ dtstartTime = new Time(Time.TIMEZONE_UTC);
+ }
+
+ dtstartTime.set(dtstart);
+ // make sure the time is printed just as a date, if all day.
+ // TODO: android.pim.Time really should take care of this for us.
+ if (allDay) {
+ dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE"));
+ dtstartTime.allDay = true;
+ dtstartTime.hour = 0;
+ dtstartTime.minute = 0;
+ dtstartTime.second = 0;
+ }
+
+ dtstartProp.setValue(dtstartTime.format2445());
+ component.addProperty(dtstartProp);
+ ICalendar.Property durationProp = new ICalendar.Property("DURATION");
+ durationProp.setValue(duration);
+ component.addProperty(durationProp);
+
+ addPropertiesForRuleStr(component, "RRULE", rruleStr);
+ addPropertyForDateStr(component, "RDATE", rdateStr);
+ addPropertiesForRuleStr(component, "EXRULE", exruleStr);
+ addPropertyForDateStr(component, "EXDATE", exdateStr);
+ return true;
+ }
+
+ private static void addPropertiesForRuleStr(ICalendar.Component component,
+ String propertyName,
+ String ruleStr) {
+ if (TextUtils.isEmpty(ruleStr)) {
+ return;
+ }
+ String[] rrules = ruleStr.split(RULE_SEPARATOR);
+ for (String rrule : rrules) {
+ ICalendar.Property prop = new ICalendar.Property(propertyName);
+ prop.setValue(rrule);
+ component.addProperty(prop);
+ }
+ }
+
+ private static void addPropertyForDateStr(ICalendar.Component component,
+ String propertyName,
+ String dateStr) {
+ if (TextUtils.isEmpty(dateStr)) {
+ return;
+ }
+
+ ICalendar.Property prop = new ICalendar.Property(propertyName);
+ String tz = null;
+ int tzidx = dateStr.indexOf(";");
+ if (tzidx != -1) {
+ tz = dateStr.substring(0, tzidx);
+ dateStr = dateStr.substring(tzidx + 1);
+ }
+ if (!TextUtils.isEmpty(tz)) {
+ prop.addParameter(new ICalendar.Parameter("TZID", tz));
+ }
+ prop.setValue(dateStr);
+ component.addProperty(prop);
+ }
+
+ private static String computeDuration(Time start,
+ ICalendar.Component component) {
+ // see if a duration is defined
+ ICalendar.Property durationProperty =
+ component.getFirstProperty("DURATION");
+ if (durationProperty != null) {
+ // just return the duration
+ return durationProperty.getValue();
+ }
+
+ // must compute a duration from the DTEND
+ ICalendar.Property dtendProperty =
+ component.getFirstProperty("DTEND");
+ if (dtendProperty == null) {
+ // no DURATION, no DTEND: 0 second duration
+ return "+P0S";
+ }
+ ICalendar.Parameter endTzidParameter =
+ dtendProperty.getFirstParameter("TZID");
+ String endTzid = (endTzidParameter == null)
+ ? start.timezone : endTzidParameter.value;
+
+ Time end = new Time(endTzid);
+ end.parse(dtendProperty.getValue());
+ long durationMillis = end.toMillis(false /* use isDst */)
+ - start.toMillis(false /* use isDst */);
+ long durationSeconds = (durationMillis / 1000);
+ return "P" + durationSeconds + "S";
+ }
+
+ private static String flattenProperties(ICalendar.Component component,
+ String name) {
+ List<ICalendar.Property> properties = component.getProperties(name);
+ if (properties == null || properties.isEmpty()) {
+ return null;
+ }
+
+ if (properties.size() == 1) {
+ return properties.get(0).getValue();
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ boolean first = true;
+ for (ICalendar.Property property : component.getProperties(name)) {
+ if (first) {
+ first = false;
+ } else {
+ // TODO: use commas. our RECUR parsing should handle that
+ // anyway.
+ sb.append(RULE_SEPARATOR);
+ }
+ sb.append(property.getValue());
+ }
+ return sb.toString();
+ }
+
+ private static String extractDates(ICalendar.Property recurrence) {
+ if (recurrence == null) {
+ return null;
+ }
+ ICalendar.Parameter tzidParam =
+ recurrence.getFirstParameter("TZID");
+ if (tzidParam != null) {
+ return tzidParam.value + ";" + recurrence.getValue();
+ }
+ return recurrence.getValue();
+ }
+}
diff --git a/core/java/android/pim/package.html b/core/java/android/pim/package.html
new file mode 100644
index 0000000..75237c9
--- /dev/null
+++ b/core/java/android/pim/package.html
@@ -0,0 +1,7 @@
+<HTML>
+<BODY>
+{@hide}
+Provides helpers for working with PIM (Personal Information Manager) data used
+by contact lists and calendars.
+</BODY>
+</HTML> \ No newline at end of file