diff options
Diffstat (limited to 'core/java/android/pim')
-rw-r--r-- | core/java/android/pim/ContactsAsyncHelper.java | 336 | ||||
-rw-r--r-- | core/java/android/pim/DateException.java | 26 | ||||
-rw-r--r-- | core/java/android/pim/EventRecurrence.java | 421 | ||||
-rw-r--r-- | core/java/android/pim/ICalendar.java | 644 | ||||
-rw-r--r-- | core/java/android/pim/RecurrenceSet.java | 398 | ||||
-rw-r--r-- | core/java/android/pim/package.html | 7 |
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 |