diff options
| author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 | 
|---|---|---|
| committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 | 
| commit | 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch) | |
| tree | d88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/pim | |
| parent | d83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff) | |
| download | frameworks_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.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 | 
