diff options
| author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 | 
|---|---|---|
| committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 | 
| commit | 54b6cfa9a9e5b861a9930af873580d6dc20f773c (patch) | |
| tree | 35051494d2af230dce54d6b31c6af8fc24091316 /core/java/android/pim | |
| download | frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.zip frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.gz frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.bz2 | |
Initial Contribution
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/DateFormat.java | 493 | ||||
| -rw-r--r-- | core/java/android/pim/DateUtils.java | 1408 | ||||
| -rw-r--r-- | core/java/android/pim/EventRecurrence.java | 420 | ||||
| -rw-r--r-- | core/java/android/pim/ICalendar.java | 643 | ||||
| -rw-r--r-- | core/java/android/pim/RecurrenceSet.java | 398 | ||||
| -rw-r--r-- | core/java/android/pim/Time.java | 570 | ||||
| -rw-r--r-- | core/java/android/pim/package.html | 7 | 
9 files changed, 4301 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/DateFormat.java b/core/java/android/pim/DateFormat.java new file mode 100644 index 0000000..802e045 --- /dev/null +++ b/core/java/android/pim/DateFormat.java @@ -0,0 +1,493 @@ +/* + * 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.Context; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.SpannedString; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** +    Utility class for producing strings with formatted date/time. + +    <p> +    This class takes as inputs a format string and a representation of a date/time. +    The format string controls how the output is generated. +    </p> +    <p> +    Formatting characters may be repeated in order to get more detailed representations +    of that field.  For instance, the format character 'M' is used to +    represent the month.  Depending on how many times that character is repeated +    you get a different representation. +    </p> +    <p> +    For the month of September:<br/> +    M -> 9<br/> +    MM -> 09<br/> +    MMM -> Sep<br/> +    MMMM -> September +    </p> +    <p> +    The effects of the duplication vary depending on the nature of the field. +    See the notes on the individual field formatters for details.  For purely numeric +    fields such as <code>HOUR</code> adding more copies of the designator will +    zero-pad the value to that number of characters. +    </p> +    <p> +    For 7 minutes past the hour:<br/> +    m -> 7<br/> +    mm -> 07<br/> +    mmm -> 007<br/> +    mmmm -> 0007 +    </p> +    <p> +    Examples for April 6, 1970 at 3:23am:<br/> +    "MM/dd/yy h:mmaa" -> "04/06/70 3:23am"<br/> +    "MMM dd, yyyy h:mmaa" -> "Apr 6, 1970 3:23am"<br/> +    "MMMM dd, yyyy h:mmaa" -> "April 6, 1970 3:23am"<br/> +    "E, MMMM dd, yyyy h:mmaa" -> "Mon, April 6, 1970 3:23am&<br/> +    "EEEE, MMMM dd, yyyy h:mmaa" -> "Monday, April 6, 1970 3:23am"<br/> +    "'Best day evar: 'M/d/yy" -> "Best day evar: 4/6/70" + */ + +public class DateFormat { +    /** +        Text in the format string that should be copied verbatim rather that +        interpreted as formatting codes must be surrounded by the <code>QUOTE</code> +        character.  If you need to embed a literal <code>QUOTE</code> character in +        the output text then use two in a row. +     */ +    public  static final char    QUOTE                  =    '\''; +     +    /** +        This designator indicates whether the <code>HOUR</code> field is before +        or after noon.  The output is lower-case. +      +        Examples: +        a -> a or p +        aa -> am or pm +     */ +    public  static final char    AM_PM                  =    'a'; + +    /** +        This designator indicates whether the <code>HOUR</code> field is before +        or after noon.  The output is capitalized. +      +        Examples: +        A -> A or P +        AA -> AM or PM +     */ +    public  static final char    CAPITAL_AM_PM          =    'A'; + +    /** +        This designator indicates the day of the month. +          +        Examples for the 9th of the month: +        d -> 9 +        dd -> 09 +     */ +    public  static final char    DATE                   =    'd'; + +    /** +        This designator indicates the name of the day of the week. +      +        Examples for Sunday: +        E -> Sun +        EEEE -> Sunday +     */ +    public  static final char    DAY                    =    'E'; + +    /** +        This designator indicates the hour of the day in 12 hour format. +      +        Examples for 3pm: +        h -> 3 +        hh -> 03 +     */ +    public  static final char    HOUR                   =    'h'; + +    /** +        This designator indicates the hour of the day in 24 hour format. +      +        Example for 3pm: +        k -> 15 + +        Examples for midnight: +        k -> 0 +        kk -> 00 +     */ +    public  static final char    HOUR_OF_DAY            =    'k'; + +    /** +        This designator indicates the minute of the hour. +      +        Examples for 7 minutes past the hour: +        m -> 7 +        mm -> 07 +     */ +    public  static final char    MINUTE                 =    'm'; + +    /** +        This designator indicates the month of the year +      +        Examples for September: +        M -> 9 +        MM -> 09 +        MMM -> Sep +        MMMM -> September +     */ +    public  static final char    MONTH                  =    'M'; + +    /** +        This designator indicates the seconds of the minute. +      +        Examples for 7 seconds past the minute: +        s -> 7 +        ss -> 07 +     */ +    public  static final char    SECONDS                =    's'; + +    /** +        This designator indicates the offset of the timezone from GMT. +      +        Example for US/Pacific timezone: +        z -> -0800 +        zz -> PST +     */ +    public  static final char    TIME_ZONE              =    'z'; + +    /** +        This designator indicates the year. +      +        Examples for 2006 +        y -> 06 +        yyyy -> 2006 +     */ +    public  static final char    YEAR                   =    'y'; + +    /** +     * @return true if the user has set the system to use a 24 hour time +     * format, else false. +     */ +    public static boolean is24HourFormat(Context context) { +        String value = Settings.System.getString(context.getContentResolver(), +                Settings.System.TIME_12_24); +        boolean b24 =  !(value == null || value.equals("12")); +        return b24; +    } + +    /** +     * Returns a {@link java.text.DateFormat} object that can format the time according +     * to the current user preference.  +     * @param context the application context +     * @return the {@link java.text.DateFormat} object that properly formats the time. +     */ +    public static final java.text.DateFormat getTimeFormat(Context context) { +        boolean b24 = is24HourFormat(context); +        return new java.text.SimpleDateFormat(b24 ? "H:mm" : "h:mm a"); +    } + +    /** +     * Returns a {@link java.text.DateFormat} object that can format the date according +     * to the current user preference. +     * @param context the application context +     * @return the {@link java.text.DateFormat} object that properly formats the date. +     */ +    public static final java.text.DateFormat getDateFormat(Context context) { +        String value = getDateFormatString(context); +        return new java.text.SimpleDateFormat(value); +    } +     +    /** +     * Returns a {@link java.text.DateFormat} object that can format the date +     * in long form (such as December 31, 1999) based on user preference. +     * @param context the application context +     * @return the {@link java.text.DateFormat} object that formats the date in long form. +     */ +    public static final java.text.DateFormat getLongDateFormat(Context context) { +        String value = getDateFormatString(context); +        if (value.indexOf('M') < value.indexOf('d')) { +            value = "MMMM dd, yyyy"; +        } else { +            value = "dd MMMM, yyyy"; +        } +        return new java.text.SimpleDateFormat(value); +    } +     +    /** +     * Gets the current date format stored as a char array. The array will contain +     * 3 elements ({@link #DATE}, {@link #MONTH}, and {@link #YEAR}) in the order     +     * preferred by the user. +     */     +    public static final char[] getDateFormatOrder(Context context) { +        char[] order = new char[] {DATE, MONTH, YEAR}; +        String value = getDateFormatString(context); +        int index = 0; +        boolean foundDate = false; +        boolean foundMonth = false; +        boolean foundYear = false; + +        for (char c : value.toCharArray()) { +            if (!foundDate && (c == DATE)) { +                foundDate = true; +                order[index] = DATE; +                index++; +            } + +            if (!foundMonth && (c == MONTH)) { +                foundMonth = true; +                order[index] = MONTH; +                index++; +            } + +            if (!foundYear && (c == YEAR)) { +                foundYear = true; +                order[index] = YEAR; +                index++; +            } +        } +        return order; +    } +     +    private static String getDateFormatString(Context context) { +        String value = Settings.System.getString(context.getContentResolver(), +                Settings.System.DATE_FORMAT); +        if (value == null || value.length() < 6) { +            value = "MM-dd-yyyy"; +        } +        return value; +    } + +    public static final CharSequence format(CharSequence inFormat, long inTimeInMillis) { +        return format(inFormat, new Date(inTimeInMillis)); +    } + +    public static final CharSequence format(CharSequence inFormat, Date inDate) { +        Calendar    c = new GregorianCalendar(); +         +        c.setTime(inDate); +         +        return format(inFormat, c); +    } + +    public static final CharSequence format(CharSequence inFormat, Calendar inDate) { +        SpannableStringBuilder      s = new SpannableStringBuilder(inFormat); +        int             c; +        int             count; + +        int len = inFormat.length(); + +        for (int i = 0; i < len; i += count) { +            int temp; + +            count = 1; +            c = s.charAt(i); + +            if (c == QUOTE) { +                count = appendQuotedText(s, i, len); +                len = s.length(); +                continue; +            } + +            while ((i + count < len) && (s.charAt(i + count) == c)) { +                count++; +            } + +            String replacement; + +            switch (c) { +                case AM_PM: +                    replacement = DateUtils.getAMPMString(inDate.get(Calendar.AM_PM)); +                    break; +                                         +                case CAPITAL_AM_PM: +                    //FIXME: this is the same as AM_PM? no capital? +                    replacement = DateUtils.getAMPMString(inDate.get(Calendar.AM_PM)); +                    break; +                 +                case DATE: +                    replacement = zeroPad(inDate.get(Calendar.DATE), count); +                    break; +                     +                case DAY: +                    temp = inDate.get(Calendar.DAY_OF_WEEK); +                    replacement = DateUtils.getDayOfWeekString(temp, +                                                               count < 4 ?  +                                                               DateUtils.LENGTH_MEDIUM :  +                                                               DateUtils.LENGTH_LONG); +                    break; +                     +                case HOUR: +                    temp = inDate.get(Calendar.HOUR); + +                    if (0 == temp) +                        temp = 12; +                     +                    replacement = zeroPad(temp, count); +                    break; +                     +                case HOUR_OF_DAY: +                    replacement = zeroPad(inDate.get(Calendar.HOUR_OF_DAY), count); +                    break; +                     +                case MINUTE: +                    replacement = zeroPad(inDate.get(Calendar.MINUTE), count); +                    break; +                     +                case MONTH: +                    replacement = getMonthString(inDate, count); +                    break; +                     +                case SECONDS: +                    replacement = zeroPad(inDate.get(Calendar.SECOND), count); +                    break; +                     +                case TIME_ZONE: +                    replacement = getTimeZoneString(inDate, count); +                    break; +                     +                case YEAR: +                    replacement = getYearString(inDate, count); +                    break; + +                default: +                    replacement = null; +                    break; +            } +             +            if (replacement != null) { +                s.replace(i, i + count, replacement); +                count = replacement.length(); // CARE: count is used in the for loop above +                len = s.length(); +            } +        } +         +        if (inFormat instanceof Spanned) +            return new SpannedString(s); +        else +            return s.toString(); +    } +     +    private static final String getMonthString(Calendar inDate, int count) { +        int month = inDate.get(Calendar.MONTH); +         +        if (count >= 4) +            return DateUtils.getMonthString(month, DateUtils.LENGTH_LONG); +        else if (count == 3) +            return DateUtils.getMonthString(month, DateUtils.LENGTH_MEDIUM); +        else { +            // Calendar.JANUARY == 0, so add 1 to month. +            return zeroPad(month+1, count); +        } +    } +         +    private static final String getTimeZoneString(Calendar inDate, int count) { +        TimeZone tz = inDate.getTimeZone(); +         +        if (count < 2) { // FIXME: shouldn't this be <= 2 ? +            return formatZoneOffset(inDate.get(Calendar.DST_OFFSET) + +                                    inDate.get(Calendar.ZONE_OFFSET), +                                    count); +        } else { +            boolean dst = inDate.get(Calendar.DST_OFFSET) != 0; +            return tz.getDisplayName(dst, TimeZone.SHORT); +        } +    } + +    private static final String formatZoneOffset(int offset, int count) { +        offset /= 1000; // milliseconds to seconds +        StringBuilder tb = new StringBuilder(); + +        if (offset < 0) { +            tb.insert(0, "-"); +            offset = -offset; +        } else { +            tb.insert(0, "+"); +        } + +        int hours = offset / 3600; +        int minutes = (offset % 3600) / 60; + +        tb.append(zeroPad(hours, 2)); +        tb.append(zeroPad(minutes, 2)); +        return tb.toString(); +    } +     +    private static final String getYearString(Calendar inDate, int count) { +        int year = inDate.get(Calendar.YEAR); +        return (count <= 2) ? zeroPad(year % 100, 2) : String.valueOf(year); +    } +    +    private static final int appendQuotedText(SpannableStringBuilder s, int i, int len) { +        if (i + 1 < len && s.charAt(i + 1) == QUOTE) { +            s.delete(i, i + 1); +            return 1; +        } + +        int count = 0; + +        // delete leading quote +        s.delete(i, i + 1); +        len--; + +        while (i < len) { +            char c = s.charAt(i); + +            if (c == QUOTE) { +                //  QUOTEQUOTE -> QUOTE +                if (i + 1 < len && s.charAt(i + 1) == QUOTE) { + +                    s.delete(i, i + 1); +                    len--; +                    count++; +                    i++; +                } else { +                    //  Closing QUOTE ends quoted text copying +                    s.delete(i, i + 1); +                    break; +                } +            } else { +                i++; +                count++; +            } +        } + +        return count; +    } + +    private static final String zeroPad(int inValue, int inMinDigits) { +        String val = String.valueOf(inValue); + +        if (val.length() < inMinDigits) { +            char[] buf = new char[inMinDigits]; + +            for (int i = 0; i < inMinDigits; i++) +                buf[i] = '0'; + +            val.getChars(0, val.length(), buf, inMinDigits - val.length()); +            val = new String(buf); +        } +        return val; +    } +} diff --git a/core/java/android/pim/DateUtils.java b/core/java/android/pim/DateUtils.java new file mode 100644 index 0000000..2a01f12 --- /dev/null +++ b/core/java/android/pim/DateUtils.java @@ -0,0 +1,1408 @@ +/* + * 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.Context; +import android.content.res.Configuration; +import android.content.res.Resources; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import com.android.internal.R; + +/** + */ +public class DateUtils +{ +    private static final String TAG = "DateUtils"; + +    private static final Object sLock = new Object(); +    private static final int[] sDaysLong = new int[] { +            com.android.internal.R.string.day_of_week_long_sunday, +            com.android.internal.R.string.day_of_week_long_monday, +            com.android.internal.R.string.day_of_week_long_tuesday, +            com.android.internal.R.string.day_of_week_long_wednesday, +            com.android.internal.R.string.day_of_week_long_thursday, +            com.android.internal.R.string.day_of_week_long_friday, +            com.android.internal.R.string.day_of_week_long_saturday, +        }; +    private static final int[] sDaysMedium = new int[] { +            com.android.internal.R.string.day_of_week_medium_sunday, +            com.android.internal.R.string.day_of_week_medium_monday, +            com.android.internal.R.string.day_of_week_medium_tuesday, +            com.android.internal.R.string.day_of_week_medium_wednesday, +            com.android.internal.R.string.day_of_week_medium_thursday, +            com.android.internal.R.string.day_of_week_medium_friday, +            com.android.internal.R.string.day_of_week_medium_saturday, +        }; +    private static final int[] sDaysShort = new int[] { +            com.android.internal.R.string.day_of_week_short_sunday, +            com.android.internal.R.string.day_of_week_short_monday, +            com.android.internal.R.string.day_of_week_short_tuesday, +            com.android.internal.R.string.day_of_week_short_wednesday, +            com.android.internal.R.string.day_of_week_short_thursday, +            com.android.internal.R.string.day_of_week_short_friday, +            com.android.internal.R.string.day_of_week_short_saturday, +        }; +    private static final int[] sDaysShorter = new int[] { +            com.android.internal.R.string.day_of_week_shorter_sunday, +            com.android.internal.R.string.day_of_week_shorter_monday, +            com.android.internal.R.string.day_of_week_shorter_tuesday, +            com.android.internal.R.string.day_of_week_shorter_wednesday, +            com.android.internal.R.string.day_of_week_shorter_thursday, +            com.android.internal.R.string.day_of_week_shorter_friday, +            com.android.internal.R.string.day_of_week_shorter_saturday, +        }; +    private static final int[] sDaysShortest = new int[] { +            com.android.internal.R.string.day_of_week_shortest_sunday, +            com.android.internal.R.string.day_of_week_shortest_monday, +            com.android.internal.R.string.day_of_week_shortest_tuesday, +            com.android.internal.R.string.day_of_week_shortest_wednesday, +            com.android.internal.R.string.day_of_week_shortest_thursday, +            com.android.internal.R.string.day_of_week_shortest_friday, +            com.android.internal.R.string.day_of_week_shortest_saturday, +        }; +    private static final int[] sMonthsLong = new int [] { +            com.android.internal.R.string.month_long_january, +            com.android.internal.R.string.month_long_february, +            com.android.internal.R.string.month_long_march, +            com.android.internal.R.string.month_long_april, +            com.android.internal.R.string.month_long_may, +            com.android.internal.R.string.month_long_june, +            com.android.internal.R.string.month_long_july, +            com.android.internal.R.string.month_long_august, +            com.android.internal.R.string.month_long_september, +            com.android.internal.R.string.month_long_october, +            com.android.internal.R.string.month_long_november, +            com.android.internal.R.string.month_long_december, +        }; +    private static final int[] sMonthsMedium = new int [] { +            com.android.internal.R.string.month_medium_january, +            com.android.internal.R.string.month_medium_february, +            com.android.internal.R.string.month_medium_march, +            com.android.internal.R.string.month_medium_april, +            com.android.internal.R.string.month_medium_may, +            com.android.internal.R.string.month_medium_june, +            com.android.internal.R.string.month_medium_july, +            com.android.internal.R.string.month_medium_august, +            com.android.internal.R.string.month_medium_september, +            com.android.internal.R.string.month_medium_october, +            com.android.internal.R.string.month_medium_november, +            com.android.internal.R.string.month_medium_december, +        }; +    private static final int[] sMonthsShortest = new int [] { +            com.android.internal.R.string.month_shortest_january, +            com.android.internal.R.string.month_shortest_february, +            com.android.internal.R.string.month_shortest_march, +            com.android.internal.R.string.month_shortest_april, +            com.android.internal.R.string.month_shortest_may, +            com.android.internal.R.string.month_shortest_june, +            com.android.internal.R.string.month_shortest_july, +            com.android.internal.R.string.month_shortest_august, +            com.android.internal.R.string.month_shortest_september, +            com.android.internal.R.string.month_shortest_october, +            com.android.internal.R.string.month_shortest_november, +            com.android.internal.R.string.month_shortest_december, +        }; +    private static final int[] sAmPm = new int[] { +            com.android.internal.R.string.am, +            com.android.internal.R.string.pm, +        }; +    private static int sFirstDay; +    private static Configuration sLastConfig; +    private static String sStatusDateFormat; +    private static String sStatusTimeFormat; +    private static String sElapsedFormatMMSS; +    private static String sElapsedFormatHMMSS; +     +    private static final String FAST_FORMAT_HMMSS = "%1$d:%2$02d:%3$02d"; +    private static final String FAST_FORMAT_MMSS = "%1$02d:%2$02d"; +    private static final char TIME_PADDING = '0'; +    private static final char TIME_SEPARATOR = ':'; +     + +    public static final long SECOND_IN_MILLIS = 1000; +    public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60; +    public static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60; +    public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24; +    public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7; +    public static final long YEAR_IN_MILLIS = WEEK_IN_MILLIS * 52; + +    // The following FORMAT_* symbols are used for specifying the format of +    // dates and times in the formatDateRange method. +    public static final int FORMAT_SHOW_TIME      = 0x00001; +    public static final int FORMAT_SHOW_WEEKDAY   = 0x00002; +    public static final int FORMAT_SHOW_YEAR      = 0x00004; +    public static final int FORMAT_NO_YEAR        = 0x00008; +    public static final int FORMAT_SHOW_DATE      = 0x00010; +    public static final int FORMAT_NO_MONTH_DAY   = 0x00020; +    public static final int FORMAT_24HOUR         = 0x00040; +    public static final int FORMAT_CAP_AMPM       = 0x00080; +    public static final int FORMAT_NO_NOON        = 0x00100; +    public static final int FORMAT_CAP_NOON       = 0x00200; +    public static final int FORMAT_NO_MIDNIGHT    = 0x00400; +    public static final int FORMAT_CAP_MIDNIGHT   = 0x00800; +    public static final int FORMAT_UTC            = 0x01000; +    public static final int FORMAT_ABBREV_TIME    = 0x02000; +    public static final int FORMAT_ABBREV_WEEKDAY = 0x04000; +    public static final int FORMAT_ABBREV_MONTH   = 0x08000; +    public static final int FORMAT_NUMERIC_DATE   = 0x10000; +    public static final int FORMAT_ABBREV_ALL     = (FORMAT_ABBREV_TIME +            | FORMAT_ABBREV_WEEKDAY | FORMAT_ABBREV_MONTH); +    public static final int FORMAT_CAP_NOON_MIDNIGHT = (FORMAT_CAP_NOON | FORMAT_CAP_MIDNIGHT); +    public static final int FORMAT_NO_NOON_MIDNIGHT = (FORMAT_NO_NOON | FORMAT_NO_MIDNIGHT); + +    // Date and time format strings that are constant and don't need to be +    // translated. +    public static final String HOUR_MINUTE_24 = "%H:%M"; +    public static final String HOUR_MINUTE_AMPM = "%-l:%M%P"; +    public static final String HOUR_MINUTE_CAP_AMPM = "%-l:%M%p"; +    public static final String HOUR_AMPM = "%-l%P"; +    public static final String HOUR_CAP_AMPM = "%-l%p"; +    public static final String MONTH_FORMAT = "%B"; +    public static final String ABBREV_MONTH_FORMAT = "%b"; +    public static final String NUMERIC_MONTH_FORMAT = "%m"; +    public static final String MONTH_DAY_FORMAT = "%-d"; +    public static final String YEAR_FORMAT = "%Y"; +    public static final String YEAR_FORMAT_TWO_DIGITS = "%g"; +    public static final String WEEKDAY_FORMAT = "%A"; +    public static final String ABBREV_WEEKDAY_FORMAT = "%a"; +     +    // This table is used to lookup the resource string id of a format string +    // used for formatting a start and end date that fall in the same year. +    // The index is constructed from a bit-wise OR of the boolean values: +    // {showTime, showYear, showWeekDay}.  For example, if showYear and +    // showWeekDay are both true, then the index would be 3. +    public static final int sameYearTable[] = { +        com.android.internal.R.string.same_year_md1_md2, +        com.android.internal.R.string.same_year_wday1_md1_wday2_md2, +        com.android.internal.R.string.same_year_mdy1_mdy2, +        com.android.internal.R.string.same_year_wday1_mdy1_wday2_mdy2, +        com.android.internal.R.string.same_year_md1_time1_md2_time2, +        com.android.internal.R.string.same_year_wday1_md1_time1_wday2_md2_time2, +        com.android.internal.R.string.same_year_mdy1_time1_mdy2_time2, +        com.android.internal.R.string.same_year_wday1_mdy1_time1_wday2_mdy2_time2, + +        // Numeric date strings +        com.android.internal.R.string.numeric_md1_md2, +        com.android.internal.R.string.numeric_wday1_md1_wday2_md2, +        com.android.internal.R.string.numeric_mdy1_mdy2, +        com.android.internal.R.string.numeric_wday1_mdy1_wday2_mdy2, +        com.android.internal.R.string.numeric_md1_time1_md2_time2, +        com.android.internal.R.string.numeric_wday1_md1_time1_wday2_md2_time2, +        com.android.internal.R.string.numeric_mdy1_time1_mdy2_time2, +        com.android.internal.R.string.numeric_wday1_mdy1_time1_wday2_mdy2_time2, +    }; +     +    // This table is used to lookup the resource string id of a format string +    // used for formatting a start and end date that fall in the same month. +    // The index is constructed from a bit-wise OR of the boolean values: +    // {showTime, showYear, showWeekDay}.  For example, if showYear and +    // showWeekDay are both true, then the index would be 3. +    public static final int sameMonthTable[] = { +        com.android.internal.R.string.same_month_md1_md2, +        com.android.internal.R.string.same_month_wday1_md1_wday2_md2, +        com.android.internal.R.string.same_month_mdy1_mdy2, +        com.android.internal.R.string.same_month_wday1_mdy1_wday2_mdy2, +        com.android.internal.R.string.same_month_md1_time1_md2_time2, +        com.android.internal.R.string.same_month_wday1_md1_time1_wday2_md2_time2, +        com.android.internal.R.string.same_month_mdy1_time1_mdy2_time2, +        com.android.internal.R.string.same_month_wday1_mdy1_time1_wday2_mdy2_time2, + +        com.android.internal.R.string.numeric_md1_md2, +        com.android.internal.R.string.numeric_wday1_md1_wday2_md2, +        com.android.internal.R.string.numeric_mdy1_mdy2, +        com.android.internal.R.string.numeric_wday1_mdy1_wday2_mdy2, +        com.android.internal.R.string.numeric_md1_time1_md2_time2, +        com.android.internal.R.string.numeric_wday1_md1_time1_wday2_md2_time2, +        com.android.internal.R.string.numeric_mdy1_time1_mdy2_time2, +        com.android.internal.R.string.numeric_wday1_mdy1_time1_wday2_mdy2_time2, +    }; + +    /** +     * Request the full spelled-out name. +     * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}. +     * @more +     * <p>e.g. "Sunday" or "January" +     */ +    public static final int LENGTH_LONG = 10; + +    /** +     * Request an abbreviated version of the name. +     * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}. +     * @more +     * <p>e.g. "Sun" or "Jan" +     */ +    public static final int LENGTH_MEDIUM = 20; + +    /** +     * Request a shorter abbreviated version of the name. +     * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}. +     * @more +     * <p>e.g. "Su" or "Jan" +     * <p>In some languages, the results returned for LENGTH_SHORT may be the same as +     * return for {@link #LENGTH_MEDIUM}. +     */ +    public static final int LENGTH_SHORT = 30; + +    /** +     * Request an even shorter abbreviated version of the name. +     * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}. +     * @more +     * <p>e.g. "M", "Tu", "Th" or "J" +     * <p>In some languages, the results returned for LENGTH_SHORTEST may be the same as +     * return for {@link #LENGTH_SHORTER}. +     */ +    public static final int LENGTH_SHORTER = 40; + +    /** +     * Request an even shorter abbreviated version of the name. +     * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}. +     * @more +     * <p>e.g. "S", "T", "T" or "J" +     * <p>In some languages, the results returned for LENGTH_SHORTEST may be the same as +     * return for {@link #LENGTH_SHORTER}. +     */ +    public static final int LENGTH_SHORTEST = 50; + + +    /** +     * Return a string for the day of the week. +     * @param dayOfWeek One of {@link #Calendar.SUNDAY Calendar.SUNDAY}, +     *               {@link #Calendar.MONDAY Calendar.MONDAY}, etc. +     * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_SHORT}, {@link #LENGTH_SHORTER} +     *               or {@link #LENGTH_SHORTEST}.  For forward compatibility, anything else +     *               will return the same as {#LENGTH_MEDIUM}. +     * @throws IndexOutOfBoundsException if the dayOfWeek is out of bounds. +     */ +    public static String getDayOfWeekString(int dayOfWeek, int abbrev) { +        int[] list; +        switch (abbrev) { +            case LENGTH_LONG:       list = sDaysLong;       break; +            case LENGTH_MEDIUM:     list = sDaysMedium;     break; +            case LENGTH_SHORT:      list = sDaysShort;      break; +            case LENGTH_SHORTER:    list = sDaysShorter;    break; +            case LENGTH_SHORTEST:   list = sDaysShortest;   break; +            default:                list = sDaysMedium;     break; +        } + +        Resources r = Resources.getSystem(); +        return r.getString(list[dayOfWeek - Calendar.SUNDAY]); +    } + +    /** +     * Return a string for AM or PM. +     * @param ampm Either {@link Calendar#AM Calendar.AM} or {@link Calendar#PM Calendar.PM}. +     * @throws IndexOutOfBoundsException if the ampm is out of bounds. +     */ +    public static String getAMPMString(int ampm) { +        Resources r = Resources.getSystem(); +        return r.getString(sAmPm[ampm - Calendar.AM]); +    } + +    /** +     * Return a string for the day of the week. +     * @param month One of {@link #Calendar.JANUARY Calendar.JANUARY}, +     *               {@link #Calendar.FEBRUARY Calendar.FEBRUARY}, etc. +     * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_SHORT}, {@link #LENGTH_SHORTER} +     *               or {@link #LENGTH_SHORTEST}.  For forward compatibility, anything else +     *               will return the same as {#LENGTH_MEDIUM}. +     */ +    public static String getMonthString(int month, int abbrev) { +        // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER.  +        // This is a shortcut to not spam the translators with too many variations +        // of the same string.  If we find that in a language the distinction +        // is necessary, we can can add more without changing this API. +        int[] list; +        switch (abbrev) { +            case LENGTH_LONG:       list = sMonthsLong;     break; +            case LENGTH_MEDIUM:     list = sMonthsMedium;   break; +            case LENGTH_SHORT:      list = sMonthsMedium;   break; +            case LENGTH_SHORTER:    list = sMonthsMedium;   break; +            case LENGTH_SHORTEST:   list = sMonthsShortest; break; +            default:                list = sMonthsMedium;   break; +        } + +        Resources r = Resources.getSystem(); +        return r.getString(list[month - Calendar.JANUARY]); +    } + +    public static CharSequence getRelativeTimeSpanString(long startTime) { +        return getRelativeTimeSpanString(startTime, System.currentTimeMillis(), MINUTE_IN_MILLIS); +    } + +    /** +     * Returns a string describing 'time' as a time relative to 'now'. +     * <p> +     * Time spans in the past are formatted like "42 minutes ago". +     * Time spans in the future are formatted like "in 42 minutes". +     * +     * @param time the time to describe, in milliseconds +     * @param now the current time in milliseconds +     * @param minResolution the minimum timespan to report. For example, a time 3 seconds in the +     *     past will be reported as "0 minutes ago" if this is set to MINUTE_IN_MILLIS. Pass one of +     *     0, MINUTE_IN_MILLIS, HOUR_IN_MILLIS, DAY_IN_MILLIS, WEEK_IN_MILLIS +     */ +    public static CharSequence getRelativeTimeSpanString(long time, long now, long minResolution) { +        Resources r = Resources.getSystem(); + +        // TODO: Assembling strings by hand like this is bad style for i18n. +        boolean past = (now > time); +        String prefix = past ? null : r.getString(com.android.internal.R.string.in); +        String postfix = past ? r.getString(com.android.internal.R.string.ago) : null; +        return getRelativeTimeSpanString(time, now, minResolution, prefix, postfix); +    } + +    public static CharSequence getRelativeTimeSpanString(long time, long now, long minResolution,  +            String prefix, String postfix) { +        Resources r = Resources.getSystem();  +         +        long duration = Math.abs(now - time); +         +        if (duration < MINUTE_IN_MILLIS && minResolution < MINUTE_IN_MILLIS) { +            long count = duration / SECOND_IN_MILLIS; +            String singular = r.getString(com.android.internal.R.string.second); +            String plural = r.getString(com.android.internal.R.string.seconds); +            return pluralizedSpan(count, singular, plural, prefix, postfix); +        } + +        if (duration < HOUR_IN_MILLIS && minResolution < HOUR_IN_MILLIS) { +            long count = duration / MINUTE_IN_MILLIS; +            String singular = r.getString(com.android.internal.R.string.minute); +            String plural = r.getString(com.android.internal.R.string.minutes); +            return pluralizedSpan(count, singular, plural, prefix, postfix); +        } + +        if (duration < DAY_IN_MILLIS && minResolution < DAY_IN_MILLIS) { +            long count = duration / HOUR_IN_MILLIS; +            String singular = r.getString(com.android.internal.R.string.hour); +            String plural = r.getString(com.android.internal.R.string.hours); +            return pluralizedSpan(count, singular, plural, prefix, postfix); +        } + +        if (duration < WEEK_IN_MILLIS && minResolution < WEEK_IN_MILLIS) { +            return getRelativeDayString(r, time, now); +        } +         +        return dateString(time); +    } +     + +    private static final String pluralizedSpan(long count, String singular, String plural,  +            String prefix, String postfix) { +        StringBuilder s = new StringBuilder(); + +        if (prefix != null) { +            s.append(prefix); +            s.append(" "); +        } +         +        s.append(count); +        s.append(' '); +        s.append(count == 0 || count > 1 ? plural : singular); +         +        if (postfix != null) { +            s.append(" "); +            s.append(postfix); +        } + +        return s.toString(); +    } + +    /** +     * Returns a string describing a day relative to the current day. For example if the day is +     * today this function returns "Today", if the day was a week ago it returns "7 days ago", and +     * if the day is in 2 weeks it returns "in 14 days". +     *  +     * @param r the resources to get the strings from +     * @param day the relative day to describe in UTC milliseconds +     * @param today the current time in UTC milliseconds +     * @return a formatting string +     */ +    private static final String getRelativeDayString(Resources r, long day, long today) { +        Time startTime = new Time(); +        startTime.set(day); +        Time currentTime = new Time(); +        currentTime.set(today); + +        int startDay = Time.getJulianDay(day, startTime.gmtoff); +        int currentDay = Time.getJulianDay(today, currentTime.gmtoff); + +        int days = Math.abs(currentDay - startDay); +        boolean past = (today > day); +         +        if (days == 1) { +            if (past) { +                return r.getString(com.android.internal.R.string.yesterday); +            } else { +                return r.getString(com.android.internal.R.string.tomorrow); +            } +        } else if (days == 0) { +            return r.getString(com.android.internal.R.string.today); +        } +         +        if (!past) { +            return r.getString(com.android.internal.R.string.daysDurationFuturePlural, days); +        } else { +            return r.getString(com.android.internal.R.string.daysDurationPastPlural, days); +        } +    } + +    private static void initFormatStrings() { +        synchronized (sLock) { +            Resources r = Resources.getSystem(); +            Configuration cfg = r.getConfiguration(); +            if (sLastConfig == null || !sLastConfig.equals(cfg)) { +                sLastConfig = cfg; +                sStatusTimeFormat = r.getString(com.android.internal.R.string.status_bar_time_format); +                sStatusDateFormat = r.getString(com.android.internal.R.string.status_bar_date_format); +                sElapsedFormatMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_mm_ss); +                sElapsedFormatHMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_h_mm_ss); +            } +        } +    } + +    /** +     * Format a time so it appears like it would in the status bar clock. +     * @deprecated use {@link #DateFormat.getTimeFormat(Context)} instead. +     * @hide +     */ +    public static final CharSequence timeString(long millis) { +        initFormatStrings(); +        return DateFormat.format(sStatusTimeFormat, millis); +    } +     +    /** +     * Format a date so it appears like it would in the status bar clock. +     * @deprecated use {@link #DateFormat.getDateFormat(Context)} instead. +     * @hide +     */ +    public static final CharSequence dateString(long startTime) { +        initFormatStrings(); +        return DateFormat.format(sStatusDateFormat, startTime); +    } + +    /** +     * Formats an elapsed time like MM:SS or H:MM:SS +     * for display on the call-in-progress screen. +     */ +    public static String formatElapsedTime(long elapsedSeconds) { +        initFormatStrings(); + +        long hours = 0; +        long minutes = 0; +        long seconds = 0; + +        if (elapsedSeconds >= 3600) { +            hours = elapsedSeconds / 3600; +            elapsedSeconds -= hours * 3600; +        } +        if (elapsedSeconds >= 60) { +            minutes = elapsedSeconds / 60; +            elapsedSeconds -= minutes * 60; +        } +        seconds = elapsedSeconds; + +        String result; +        if (hours > 0) { +            return formatElapsedTime(sElapsedFormatHMMSS, hours, minutes, seconds); +        } else { +            return formatElapsedTime(sElapsedFormatMMSS, minutes, seconds); +        } +    } + +    /** +     * Fast formatting of h:mm:ss +     */ +    private static String formatElapsedTime(String format, long hours, long minutes, long seconds) { +        if (FAST_FORMAT_HMMSS.equals(format)) { +            StringBuffer sb = new StringBuffer(16); +            sb.append(hours); +            sb.append(TIME_SEPARATOR); +            if (minutes < 10) {  +                sb.append(TIME_PADDING); +            } else { +                sb.append(toDigitChar(minutes / 10)); +            } +            sb.append(toDigitChar(minutes % 10)); +            sb.append(TIME_SEPARATOR); +            if (seconds < 10) { +                sb.append(TIME_PADDING); +            } else { +                sb.append(toDigitChar(seconds / 10)); +            } +            sb.append(toDigitChar(seconds % 10)); +            return sb.toString(); +        } else { +            return String.format(format, hours, minutes, seconds); +        } +    } + +    /** +     * Fast formatting of m:ss +     */ +    private static String formatElapsedTime(String format, long minutes, long seconds) { +        if (FAST_FORMAT_MMSS.equals(format)) { +            StringBuffer sb = new StringBuffer(16); +            if (minutes < 10) {  +                sb.append(TIME_PADDING); +            } else { +                sb.append(toDigitChar(minutes / 10)); +            } +            sb.append(toDigitChar(minutes % 10)); +            sb.append(TIME_SEPARATOR); +            if (seconds < 10) { +                sb.append(TIME_PADDING); +            } else { +                sb.append(toDigitChar(seconds / 10)); +            } +            sb.append(toDigitChar(seconds % 10)); +            return sb.toString(); +        } else { +            return String.format(format, minutes, seconds); +        } +    } + +    private static char toDigitChar(long digit) { +        return (char) (digit + '0'); +    } +     +    /* +     * Format a date / time such that if the then is on the same day as now, it shows +     * just the time and if it's a different day, it shows just the date. +     *  +     * <p>The parameters dateFormat and timeFormat should each be one of +     * {@link java.text.DateFormat#DEFAULT}, +     * {@link java.text.DateFormat#FULL}, +     * {@link java.text.DateFormat#LONG}, +     * {@link java.text.DateFormat#MEDIUM} +     * or +     * {@link java.text.DateFormat#SHORT} +     * +     * @param then the date to format +     * @param now the base time +     * @param dateStyle how to format the date portion. +     * @param timeStyle how to format the time portion. +     */ +    public static final CharSequence formatSameDayTime(long then, long now, +            int dateStyle, int timeStyle) { +        Calendar thenCal = new GregorianCalendar(); +        thenCal.setTimeInMillis(then); +        Date thenDate = thenCal.getTime(); +        Calendar nowCal = new GregorianCalendar(); +        nowCal.setTimeInMillis(now); + +        java.text.DateFormat f; + +        if (thenCal.get(Calendar.YEAR) == nowCal.get(Calendar.YEAR) +                && thenCal.get(Calendar.MONTH) == nowCal.get(Calendar.MONTH) +                && thenCal.get(Calendar.DAY_OF_MONTH) == nowCal.get(Calendar.DAY_OF_MONTH)) { +            f = java.text.DateFormat.getTimeInstance(timeStyle); +        } else { +            f = java.text.DateFormat.getDateInstance(dateStyle); +        } +        return f.format(thenDate); +    } + +    /** +     * @hide +     * @deprecated use {@link android.pim.Time} +     */ +    public static Calendar newCalendar(boolean zulu) +    { +        if (zulu) +            return Calendar.getInstance(TimeZone.getTimeZone("GMT")); + +        return Calendar.getInstance(); +    } + +    /** +     * @return true if the supplied when is today else false +     */ +    public static boolean isToday(long when) { +        Time time = new Time(); +        time.set(when); +         +        int thenYear = time.year; +        int thenMonth = time.month; +        int thenMonthDay = time.monthDay; + +        time.set(System.currentTimeMillis()); +        return (thenYear == time.year) +                && (thenMonth == time.month)  +                && (thenMonthDay == time.monthDay); +    } +     +    /** +     * @hide +     * @deprecated use {@link android.pim.Time} +     */ +    private static final int ctoi(String str, int index) +                                                throws DateException +    { +        char c = str.charAt(index); +        if (c >= '0' && c <= '9') { +            return (int)(c - '0'); +        } +        throw new DateException("Expected numeric character.  Got '" + +                                            c + "'"); +    } + +    /** +     * @hide +     * @deprecated use {@link android.pim.Time} +     */ +    private static final int check(int lowerBound, int upperBound, int value) +                                                throws DateException +    { +        if (value >= lowerBound && value <= upperBound) { +            return value; +        } +        throw new DateException("field out of bounds.  max=" + upperBound +                                        + " value=" + value); +    } + +    /** +     * @hide +     * @deprecated use {@link android.pim.Time} +     * Return true if this date string is local time +     */ +    public static boolean isUTC(String s) +    { +        if (s.length() == 16 && s.charAt(15) == 'Z') { +            return true; +        } +        if (s.length() == 9 && s.charAt(8) == 'Z') { +            // XXX not sure if this case possible/valid +            return true; +        } +        return false; +    } + + +    // note that month in Calendar is 0 based and in all other human +    // representations, it's 1 based. +    // Returns if the Z was present, meaning that the time is in UTC +    /** +     * @hide +     * @deprecated use {@link android.pim.Time} +     */ +    public static boolean parseDateTime(String str, Calendar cal) +                                                throws DateException +    { +        int len = str.length(); +        boolean dateTime = (len == 15 || len == 16) && str.charAt(8) == 'T'; +        boolean justDate = len == 8; +        if (dateTime || justDate) { +            cal.clear(); +            cal.set(Calendar.YEAR,  +                            ctoi(str, 0)*1000 + ctoi(str, 1)*100 +                            + ctoi(str, 2)*10 + ctoi(str, 3)); +            cal.set(Calendar.MONTH, +                            check(0, 11, ctoi(str, 4)*10 + ctoi(str, 5) - 1)); +            cal.set(Calendar.DAY_OF_MONTH, +                            check(1, 31, ctoi(str, 6)*10 + ctoi(str, 7))); +            if (dateTime) { +                cal.set(Calendar.HOUR_OF_DAY, +                            check(0, 23, ctoi(str, 9)*10 + ctoi(str, 10))); +                cal.set(Calendar.MINUTE, +                            check(0, 59, ctoi(str, 11)*10 + ctoi(str, 12))); +                cal.set(Calendar.SECOND, +                            check(0, 59, ctoi(str, 13)*10 + ctoi(str, 14))); +            } +            if (justDate) { +                cal.set(Calendar.HOUR_OF_DAY, 0); +                cal.set(Calendar.MINUTE, 0); +                cal.set(Calendar.SECOND, 0); +                return true; +            } +            if (len == 15) { +                return false; +            } +            if (str.charAt(15) == 'Z') { +                return true; +            } +        } +        throw new DateException("Invalid time (expected " +                                + "YYYYMMDDThhmmssZ? got '" + str + "')."); +    } + +    /** +     * Given a timezone string which can be null, and a dateTime string, +     * set that time into a calendar. +     * @hide +     * @deprecated use {@link android.pim.Time} +     */ +    public static void parseDateTime(String tz, String dateTime, Calendar out) +                                                throws DateException +    { +        TimeZone timezone; +        if (DateUtils.isUTC(dateTime)) { +            timezone = TimeZone.getTimeZone("UTC"); +        } +        else if (tz == null) { +            timezone = TimeZone.getDefault(); +        } +        else { +            timezone = TimeZone.getTimeZone(tz); +        } + +        Calendar local = new GregorianCalendar(timezone); +        DateUtils.parseDateTime(dateTime, local); + +        out.setTimeInMillis(local.getTimeInMillis()); +    } + + +    /** +     * Return a string containing the date and time in RFC2445 format. +     * Ensures that the time is written in UTC.  The Calendar class doesn't +     * really help out with this, so this is slower than it ought to be. +     * +     * @param cal the date and time to write +     * @hide +     * @deprecated use {@link android.pim.Time} +     */ +    public static String writeDateTime(Calendar cal) +    { +        TimeZone tz = TimeZone.getTimeZone("GMT"); +        GregorianCalendar c = new GregorianCalendar(tz); +        c.setTimeInMillis(cal.getTimeInMillis()); +        return writeDateTime(c, true); +    } + +    /** +     * Return a string containing the date and time in RFC2445 format. +     * +     * @param cal the date and time to write +     * @param zulu If the calendar is in UTC, pass true, and a Z will +     * be written at the end as per RFC2445.  Otherwise, the time is +     * considered in localtime. +     * @hide +     * @deprecated use {@link android.pim.Time} +     */ +    public static String writeDateTime(Calendar cal, boolean zulu) +    { +        StringBuilder sb = new StringBuilder(); +        sb.ensureCapacity(16); +        if (zulu) { +            sb.setLength(16); +            sb.setCharAt(15, 'Z'); +        } else { +            sb.setLength(15); +        } +        return writeDateTime(cal, sb); +    } + +    /** +     * Return a string containing the date and time in RFC2445 format. +     * +     * @param cal the date and time to write +     * @param sb a StringBuilder to use.  It is assumed that setLength +     *           has already been called on sb to the appropriate length +     *           which is sb.setLength(zulu ? 16 : 15) +     * @hide +     * @deprecated use {@link android.pim.Time} +     */ +    public static String writeDateTime(Calendar cal, StringBuilder sb) +    { +        int n; +        +        n = cal.get(Calendar.YEAR); +        sb.setCharAt(3, (char)('0'+n%10)); +        n /= 10; +        sb.setCharAt(2, (char)('0'+n%10)); +        n /= 10; +        sb.setCharAt(1, (char)('0'+n%10)); +        n /= 10; +        sb.setCharAt(0, (char)('0'+n%10)); + +        n = cal.get(Calendar.MONTH) + 1; +        sb.setCharAt(5, (char)('0'+n%10)); +        n /= 10; +        sb.setCharAt(4, (char)('0'+n%10)); + +        n = cal.get(Calendar.DAY_OF_MONTH); +        sb.setCharAt(7, (char)('0'+n%10)); +        n /= 10; +        sb.setCharAt(6, (char)('0'+n%10)); + +        sb.setCharAt(8, 'T'); + +        n = cal.get(Calendar.HOUR_OF_DAY); +        sb.setCharAt(10, (char)('0'+n%10)); +        n /= 10; +        sb.setCharAt(9, (char)('0'+n%10)); + +        n = cal.get(Calendar.MINUTE); +        sb.setCharAt(12, (char)('0'+n%10)); +        n /= 10; +        sb.setCharAt(11, (char)('0'+n%10)); + +        n = cal.get(Calendar.SECOND); +        sb.setCharAt(14, (char)('0'+n%10)); +        n /= 10; +        sb.setCharAt(13, (char)('0'+n%10)); + +        return sb.toString(); +    } + +    /** +     * @hide +     * @deprecated use {@link android.pim.Time} +     */ +    public static void assign(Calendar lval, Calendar rval) +    { +        // there should be a faster way. +        lval.clear(); +        lval.setTimeInMillis(rval.getTimeInMillis()); +    } + +    /** +     * Creates a string describing a date/time range.  The flags argument +     * is a bitmask of options from the following list: +     *  +     * <ul> +     *   <li>FORMAT_SHOW_TIME</li> +     *   <li>FORMAT_SHOW_WEEKDAY</li> +     *   <li>FORMAT_SHOW_YEAR</li> +     *   <li>FORMAT_NO_YEAR</li> +     *   <li>FORMAT_SHOW_DATE</li> +     *   <li>FORMAT_NO_MONTH_DAY</li> +     *   <li>FORMAT_24HOUR</li> +     *   <li>FORMAT_CAP_AMPM</li> +     *   <li>FORMAT_NO_NOON</li> +     *   <li>FORMAT_CAP_NOON</li> +     *   <li>FORMAT_NO_MIDNIGHT</li> +     *   <li>FORMAT_CAP_MIDNIGHT</li> +     *   <li>FORMAT_UTC</li> +     *   <li>FORMAT_ABBREV_TIME</li> +     *   <li>FORMAT_ABBREV_WEEKDAY</li> +     *   <li>FORMAT_ABBREV_MONTH</li> +     *   <li>FORMAT_ABBREV_ALL</li> +     *   <li>FORMAT_NUMERIC_DATE</li> +     * </ul> +     *  +     * <p> +     * If FORMAT_SHOW_TIME is set, the time is shown as part of the date range. +     * If the start and end time are the same, then just the start time is +     * shown. +     *  +     * <p> +     * If FORMAT_SHOW_WEEKDAY is set, then the weekday is shown. +     *  +     * <p> +     * If FORMAT_SHOW_YEAR is set, then the year is always shown. +     * If FORMAT_NO_YEAR is set, then the year is not shown. +     * If neither FORMAT_SHOW_YEAR nor FORMAT_NO_YEAR are set, then the year +     * is shown only if it is different from the current year, or if the start +     * and end dates fall on different years. +     *  +     * <p> +     * Normally the date is shown unless the start and end day are the same. +     * If FORMAT_SHOW_DATE is set, then the date is always shown, even for +     * same day ranges. +     *  +     * <p> +     * If FORMAT_NO_MONTH_DAY is set, then if the date is shown, just the +     * month name will be shown, not the day of the month.  For example, +     * "January, 2008" instead of "January 6 - 12, 2008". +     *  +     * <p> +     * If FORMAT_CAP_AMPM is set and 12-hour time is used, then the "AM" +     * and "PM" are capitalized. +     *  +     * <p> +     * If FORMAT_NO_NOON is set and 12-hour time is used, then "12pm" is +     * shown instead of "noon". +     *  +     * <p> +     * If FORMAT_CAP_NOON is set and 12-hour time is used, then "Noon" is +     * shown instead of "noon". +     *  +     * <p> +     * If FORMAT_NO_MIDNIGHT is set and 12-hour time is used, then "12am" is +     * shown instead of "midnight". +     *  +     * <p> +     * If FORMAT_CAP_NOON is set and 12-hour time is used, then "Midnight" is +     * shown instead of "midnight". +     *  +     * <p> +     * If FORMAT_24HOUR is set and the time is shown, then the time is +     * shown in the 24-hour time format. +     *  +     * <p> +     * If FORMAT_UTC is set, then the UTC timezone is used for the start +     * and end milliseconds. +     *  +     * <p> +     * If FORMAT_ABBREV_TIME is set and FORMAT_24HOUR is not set, then the +     * start and end times (if shown) are abbreviated by not showing the minutes +     * if they are zero.  For example, instead of "3:00pm" the time would be +     * abbreviated to "3pm". +     *  +     * <p> +     * If FORMAT_ABBREV_WEEKDAY is set, then the weekday (if shown) is +     * abbreviated to a 3-letter string. +     *  +     * <p> +     * If FORMAT_ABBREV_MONTH is set, then the month (if shown) is abbreviated +     * to a 3-letter string. +     *  +     * <p> +     * If FORMAT_ABBREV_ALL is set, then the weekday and the month (if shown) +     * are abbreviated to 3-letter strings. +     *  +     * <p> +     * If FORMAT_NUMERIC_DATE is set, then the date is shown in numeric format +     * instead of using the name of the month.  For example, "12/31/2008" +     * instead of "December 31, 2008". +     *  +     * <p> +     * Example output strings: +     * <ul> +     *   <li>10:15am</li> +     *   <li>3:00pm - 4:00pm</li> +     *   <li>3pm - 4pm</li> +     *   <li>3PM - 4PM</li> +     *   <li>08:00 - 17:00</li> +     *   <li>Oct 9</li> +     *   <li>Tue, Oct 9</li> +     *   <li>October 9, 2007</li> +     *   <li>Oct 9 - 10</li> +     *   <li>Oct 9 - 10, 2007</li> +     *   <li>Oct 28 - Nov 3, 2007</li> +     *   <li>Dec 31, 2007 - Jan 1, 2008</li> +     *   <li>Oct 9, 8:00am - Oct 10, 5:00pm</li> +     * </ul> +     * @param startMillis the start time in UTC milliseconds +     * @param endMillis the end time in UTC milliseconds +     * @param flags a bit mask of options +     *    +     * @return a string with the formatted date/time range. +     */ +    public static String formatDateRange(long startMillis, long endMillis, int flags) { +        Resources res = Resources.getSystem(); +        boolean showTime = (flags & FORMAT_SHOW_TIME) != 0; +        boolean showWeekDay = (flags & FORMAT_SHOW_WEEKDAY) != 0; +        boolean showYear = (flags & FORMAT_SHOW_YEAR) != 0; +        boolean noYear = (flags & FORMAT_NO_YEAR) != 0; +        boolean useUTC = (flags & FORMAT_UTC) != 0; +        boolean abbrevWeekDay = (flags & FORMAT_ABBREV_WEEKDAY) != 0; +        boolean abbrevMonth = (flags & FORMAT_ABBREV_MONTH) != 0; +        boolean use24Hour = (flags & FORMAT_24HOUR) != 0; +        boolean noMonthDay = (flags & FORMAT_NO_MONTH_DAY) != 0; +        boolean numericDate = (flags & FORMAT_NUMERIC_DATE) != 0; +     +        Time startDate; +        Time endDate; +         +        if (useUTC) { +            startDate = new Time(Time.TIMEZONE_UTC); +            endDate = new Time(Time.TIMEZONE_UTC); +        } else { +            startDate = new Time(); +            endDate = new Time(); +        } +         +        startDate.set(startMillis); +        endDate.set(endMillis); +        int startJulianDay = Time.getJulianDay(startMillis, startDate.gmtoff); +        int endJulianDay = Time.getJulianDay(endMillis, endDate.gmtoff); +        int dayDistance = endJulianDay - startJulianDay; +         +        // If the end date ends at 12am at the beginning of a day, +        // then modify it to make it look like it ends at midnight on +        // the previous day.  This will allow us to display "8pm - midnight", +        // for example, instead of "Nov 10, 8pm - Nov 11, 12am". But we only do +        // this if it is midnight of the same day as the start date because +        // for multiple-day events, an end time of "midnight on Nov 11" is +        // ambiguous and confusing (is that midnight the start of Nov 11, or +        // the end of Nov 11?). +        // If we are not showing the time then also adjust the end date +        // for multiple-day events.  This is to allow us to display, for +        // example, "Nov 10 -11" for an event with an start date of Nov 10 +        // and an end date of Nov 12 at 00:00. +        // If the start and end time are the same, then skip this and don't +        // adjust the date. +        if ((endDate.hour | endDate.minute | endDate.second) == 0 +                && (!showTime || dayDistance <= 1) && (startMillis != endMillis)) { +            endDate.monthDay -= 1; +            endDate.normalize(true /* ignore isDst */); +        } +         +        int startDay = startDate.monthDay; +        int startMonthNum = startDate.month; +        int startYear = startDate.year; +     +        int endDay = endDate.monthDay; +        int endMonthNum = endDate.month; +        int endYear = endDate.year; +     +        String startWeekDayString = ""; +        String endWeekDayString = ""; +        if (showWeekDay) { +            String weekDayFormat = ""; +            if (abbrevWeekDay) { +                weekDayFormat = ABBREV_WEEKDAY_FORMAT; +            } else { +                weekDayFormat = WEEKDAY_FORMAT; +            } +            startWeekDayString = startDate.format(weekDayFormat); +            endWeekDayString = endDate.format(weekDayFormat); +        } +         +        String startTimeString = ""; +        String endTimeString = ""; +        if (showTime) { +            String startTimeFormat = ""; +            String endTimeFormat = ""; +            if (use24Hour) { +                startTimeFormat = HOUR_MINUTE_24; +                endTimeFormat = HOUR_MINUTE_24; +            } else { +                boolean abbrevTime = (flags & FORMAT_ABBREV_TIME) != 0; +                boolean capAMPM = (flags & FORMAT_CAP_AMPM) != 0; +                boolean noNoon = (flags & FORMAT_NO_NOON) != 0; +                boolean capNoon = (flags & FORMAT_CAP_NOON) != 0; +                boolean noMidnight = (flags & FORMAT_NO_MIDNIGHT) != 0; +                boolean capMidnight = (flags & FORMAT_CAP_MIDNIGHT) != 0; +     +                boolean startOnTheHour = startDate.minute == 0 && startDate.second == 0; +                boolean endOnTheHour = endDate.minute == 0 && endDate.second == 0; +                if (abbrevTime && startOnTheHour) { +                    if (capAMPM) { +                        startTimeFormat = HOUR_CAP_AMPM; +                    } else { +                        startTimeFormat = HOUR_AMPM; +                    } +                } else { +                    if (capAMPM) { +                        startTimeFormat = HOUR_MINUTE_CAP_AMPM; +                    } else { +                        startTimeFormat = HOUR_MINUTE_AMPM; +                    } +                } +                if (abbrevTime && endOnTheHour) { +                    if (capAMPM) { +                        endTimeFormat = HOUR_CAP_AMPM; +                    } else { +                        endTimeFormat = HOUR_AMPM; +                    } +                } else { +                    if (capAMPM) { +                        endTimeFormat = HOUR_MINUTE_CAP_AMPM; +                    } else { +                        endTimeFormat = HOUR_MINUTE_AMPM; +                    } +                } +                 +                if (startDate.hour == 12 && startOnTheHour && !noNoon) { +                    if (capNoon) { +                        startTimeFormat = res.getString(com.android.internal.R.string.Noon); +                    } else { +                        startTimeFormat = res.getString(com.android.internal.R.string.noon); +                    } +                    // Don't show the start time starting at midnight.  Show +                    // 12am instead. +                } +                 +                if (endDate.hour == 12 && endOnTheHour && !noNoon) { +                    if (capNoon) { +                        endTimeFormat = res.getString(com.android.internal.R.string.Noon); +                    } else { +                        endTimeFormat = res.getString(com.android.internal.R.string.noon); +                    } +                } else if (endDate.hour == 0 && endOnTheHour && !noMidnight) { +                    if (capMidnight) { +                        endTimeFormat = res.getString(com.android.internal.R.string.Midnight); +                    } else { +                        endTimeFormat = res.getString(com.android.internal.R.string.midnight); +                    } +                } +            } +            startTimeString = startDate.format(startTimeFormat); +            endTimeString = endDate.format(endTimeFormat); +        } +         +        // Get the current year +        long millis = System.currentTimeMillis(); +        Time time = new Time(); +        time.set(millis); +        int currentYear = time.year; +     +        // Show the year if the user specified FORMAT_SHOW_YEAR or if +        // the starting and end years are different from each other +        // or from the current year.  But don't show the year if the +        // user specified FORMAT_NO_YEAR; +        showYear = showYear || (!noYear && (startYear != endYear || startYear != currentYear)); +         +        String defaultDateFormat, fullFormat, dateRange; +        if (numericDate) { +            defaultDateFormat = res.getString(com.android.internal.R.string.numeric_date); +        } else if (showYear) { +            if (abbrevMonth) { +                if (noMonthDay) { +                    defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month_year); +                } else { +                    defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month_day_year); +                } +            } else { +                if (noMonthDay) { +                    defaultDateFormat = res.getString(com.android.internal.R.string.month_year); +                } else { +                    defaultDateFormat = res.getString(com.android.internal.R.string.month_day_year); +                } +            } +        } else { +            if (abbrevMonth) { +                if (noMonthDay) { +                    defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month); +                } else { +                    defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month_day); +                } +            } else { +                if (noMonthDay) { +                    defaultDateFormat = res.getString(com.android.internal.R.string.month); +                } else { +                    defaultDateFormat = res.getString(com.android.internal.R.string.month_day); +                } +            } +        } +         +        if (showWeekDay) { +            if (showTime) { +                fullFormat = res.getString(com.android.internal.R.string.wday1_date1_time1_wday2_date2_time2); +            } else { +                fullFormat = res.getString(com.android.internal.R.string.wday1_date1_wday2_date2); +            } +        } else { +            if (showTime) { +                fullFormat = res.getString(com.android.internal.R.string.date1_time1_date2_time2); +            } else { +                fullFormat = res.getString(com.android.internal.R.string.date1_date2); +            } +        } +         +        if (noMonthDay && startMonthNum == endMonthNum) { +            // Example: "January, 2008" +            String startDateString = startDate.format(defaultDateFormat); +            return startDateString; +        } +     +        if (startYear != endYear || noMonthDay) { +            // Different year or we are not showing the month day number. +            // Example: "December 31, 2007 - January 1, 2008" +            // Or: "January - February, 2008" +            String startDateString = startDate.format(defaultDateFormat); +            String endDateString = endDate.format(defaultDateFormat); +     +            // The values that are used in a fullFormat string are specified +            // by position. +            dateRange = String.format(fullFormat, +                    startWeekDayString, startDateString, startTimeString, +                    endWeekDayString, endDateString, endTimeString); +            return dateRange; +        } +         +        // Get the month, day, and year strings for the start and end dates +        String monthFormat; +        if (numericDate) { +            monthFormat = NUMERIC_MONTH_FORMAT; +        } else if (abbrevMonth) { +            monthFormat = ABBREV_MONTH_FORMAT; +        } else { +            monthFormat = MONTH_FORMAT; +        } +        String startMonthString = startDate.format(monthFormat); +        String startMonthDayString = startDate.format(MONTH_DAY_FORMAT); +        String startYearString = startDate.format(YEAR_FORMAT); +        String endMonthString = endDate.format(monthFormat); +        String endMonthDayString = endDate.format(MONTH_DAY_FORMAT); +        String endYearString = endDate.format(YEAR_FORMAT); +         +        if (startMonthNum != endMonthNum) { +            // Same year, different month. +            // Example: "October 28 - November 3" +            // or: "Wed, Oct 31 - Sat, Nov 3, 2007" +            // or: "Oct 31, 8am - Sat, Nov 3, 2007, 5pm" +             +            int index = 0; +            if (showWeekDay) index = 1; +            if (showYear) index += 2; +            if (showTime) index += 4; +            if (numericDate) index += 8; +            int resId = sameYearTable[index]; +            fullFormat = res.getString(resId); +             +            // The values that are used in a fullFormat string are specified +            // by position. +            dateRange = String.format(fullFormat, +                    startWeekDayString, startMonthString, startMonthDayString, +                    startYearString, startTimeString, +                    endWeekDayString, endMonthString, endMonthDayString, +                    endYearString, endTimeString); +            return dateRange; +        } +     +        if (startDay != endDay) { +            // Same month, different day. +            int index = 0; +            if (showWeekDay) index = 1; +            if (showYear) index += 2; +            if (showTime) index += 4; +            if (numericDate) index += 8; +            int resId = sameMonthTable[index]; +            fullFormat = res.getString(resId); +             +            // The values that are used in a fullFormat string are specified +            // by position. +            dateRange = String.format(fullFormat, +                    startWeekDayString, startMonthString, startMonthDayString, +                    startYearString, startTimeString, +                    endWeekDayString, endMonthString, endMonthDayString, +                    endYearString, endTimeString); +            return dateRange; +        } +         +        // Same start and end day +        boolean showDate = (flags & FORMAT_SHOW_DATE) != 0; +         +        // If nothing was specified, then show the date. +        if (!showTime && !showDate && !showWeekDay) showDate = true; +         +        // Compute the time string (example: "10:00 - 11:00 am") +        String timeString = ""; +        if (showTime) { +            // If the start and end time are the same, then just show the +            // start time. +            if (startMillis == endMillis) { +                // Same start and end time. +                // Example: "10:15 AM" +                timeString = startTimeString; +            } else { +                // Example: "10:00 - 11:00 am" +                String timeFormat = res.getString(com.android.internal.R.string.time1_time2); +                timeString = String.format(timeFormat, startTimeString, endTimeString); +            } +        } +     +        // Figure out which full format to use. +        fullFormat = ""; +        String dateString = ""; +        if (showDate) { +            dateString = startDate.format(defaultDateFormat); +            if (showWeekDay) { +                if (showTime) { +                    // Example: "10:00 - 11:00 am, Tue, Oct 9" +                    fullFormat = res.getString(com.android.internal.R.string.time_wday_date); +                } else { +                    // Example: "Tue, Oct 9" +                    fullFormat = res.getString(com.android.internal.R.string.wday_date); +                } +            } else { +                if (showTime) { +                    // Example: "10:00 - 11:00 am, Oct 9" +                    fullFormat = res.getString(com.android.internal.R.string.time_date); +                } else { +                    // Example: "Oct 9" +                    return dateString; +                } +            } +        } else if (showWeekDay) { +            if (showTime) { +                // Example: "10:00 - 11:00 am, Tue" +                fullFormat = res.getString(com.android.internal.R.string.time_wday); +            } else { +                // Example: "Tue" +                return startWeekDayString; +            } +        } else if (showTime) { +            return timeString; +        } +     +        // The values that are used in a fullFormat string are specified +        // by position. +        dateRange = String.format(fullFormat, timeString, startWeekDayString, dateString); +        return dateRange; +    } + +    /** +     * @return a relative time string to display the time expressed by millis.  Times +     * are counted starting at midnight, which means that assuming that the current +     * time is March 31st, 0:30: +     * "millis=0:10 today" will be displayed as "0:10"  +     * "millis=11:30pm the day before" will be displayed as "Mar 30" +     * A similar scheme is used to dates that are a week, a month or more than a year old.  +     *  +     * @param withPreposition If true, the string returned will include the correct  +     * preposition ("at 9:20am", "in 2008" or "on May 29"). +     */ +    public static CharSequence getRelativeTimeSpanString(Context c, long millis, +            boolean withPreposition) { + +        long span = System.currentTimeMillis() - millis; + +        Resources res = c.getResources(); +        if (sNowTime == null) { +            sNowTime = new Time(); +            sThenTime = new Time(); +            sMonthDayFormat = res.getString(com.android.internal.R.string.abbrev_month_day); +        } + +        sNowTime.setToNow(); +        sThenTime.set(millis); + +        if (span < DAY_IN_MILLIS && sNowTime.weekDay == sThenTime.weekDay) { +            // Same day +            return getPrepositionDate(res, sThenTime, R.string.preposition_for_time, +                    HOUR_MINUTE_CAP_AMPM, withPreposition); +        } else if (sNowTime.year != sThenTime.year) { +            // Different years +            // TODO: take locale into account so that the display will adjust correctly. +            return getPrepositionDate(res, sThenTime, R.string.preposition_for_year, +                    NUMERIC_MONTH_FORMAT + "/" + MONTH_DAY_FORMAT + "/" + YEAR_FORMAT_TWO_DIGITS, +                    withPreposition); +        } else { +            // Default +            return getPrepositionDate(res, sThenTime, R.string.preposition_for_date, +                sMonthDayFormat, withPreposition); +        } +    } +     +    /** +     * @return A date string suitable for display based on the format and including the +     * date preposition if withPreposition is true. +     */ +    private static String getPrepositionDate(Resources res, Time thenTime, int id, +            String formatString, boolean withPreposition) { +        String result = thenTime.format(formatString); +        return withPreposition ? res.getString(id, result) : result; +    } +     +    public static CharSequence getRelativeTimeSpanString(Context c, long millis) { +        return getRelativeTimeSpanString(c, millis, false /* no preposition */); +    } +     +    private static Time sNowTime; +    private static Time sThenTime; +    private static String sMonthDayFormat; +} diff --git a/core/java/android/pim/EventRecurrence.java b/core/java/android/pim/EventRecurrence.java new file mode 100644 index 0000000..ad671f6 --- /dev/null +++ b/core/java/android/pim/EventRecurrence.java @@ -0,0 +1,420 @@ +/* + * 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 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..4a5d7e4 --- /dev/null +++ b/core/java/android/pim/ICalendar.java @@ -0,0 +1,643 @@ +/* + * 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) { +        // first we deal with line folding, by replacing all "\r\n " strings +        // with nothing +        text = text.replaceAll("\r\n ", ""); + +        // it's supposed to be \r\n, but not everyone does that +        text = text.replaceAll("\r\n", "\n"); +        text = text.replaceAll("\r", "\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; +        char c = line.charAt(state.index); +        if (c != ':') { +            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..c02ff52 --- /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.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); +        boolean rdateNotInUtc = !tz.equals(Time.TIMEZONE_UTC); +        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.parse2445(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.parse2445(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.parse2445(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/Time.java b/core/java/android/pim/Time.java new file mode 100644 index 0000000..59ba87b --- /dev/null +++ b/core/java/android/pim/Time.java @@ -0,0 +1,570 @@ +/* + * 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 java.util.TimeZone; + +/** + * {@hide} + * + * The Time class is a faster replacement for the java.util.Calendar and + * java.util.GregorianCalendar classes. An instance of the Time class represents + * a moment in time, specified with second precision. It is modelled after + * struct tm, and in fact, uses struct tm to implement most of the + * functionality. + */ +public class Time { +    public static final String TIMEZONE_UTC = "UTC"; + +    /** +     * The Julian day of the epoch, that is, January 1, 1970 on the Gregorian +     * calendar. +     */ +    public static final int EPOCH_JULIAN_DAY = 2440588; + +    /** +     * True if this is an allDay event. The hour, minute, second fields are +     * all zero, and the date is displayed the same in all time zones. +     */ +    public boolean allDay; + +    /** +     * Seconds [0-61] (2 leap seconds allowed) +     */ +    public int second; + +    /** +     * Minute [0-59] +     */ +    public int minute; + +    /** +     * Hour of day [0-23] +     */ +    public int hour; + +    /** +     * Day of month [1-31] +     */ +    public int monthDay; + +    /** +     * Month [0-11] +     */ +    public int month; + +    /** +     * Year. TBD. Is this years since 1900 like in struct tm? +     */ +    public int year; + +    /** +     * Day of week [0-6] +     */ +    public int weekDay; + +    /** +     * Day of year [0-365] +     */ +    public int yearDay; + +    /** +     * This time is in daylight savings time. One of: +     * <ul> +     * <li><b>positive</b> - in dst</li> +     * <li><b>0</b> - not in dst</li> +     * <li><b>negative</b> - unknown</li> +     */ +    public int isDst; + +    /** +     * Offset from UTC (in seconds). +     */ +    public long gmtoff; + +    /** +     * The timezone for this Time.  Should not be null. +     */ +    public String timezone; + +    /* +     * Define symbolic constants for accessing the fields in this class. Used in +     * getActualMaximum(). +     */ +    public static final int SECOND = 1; +    public static final int MINUTE = 2; +    public static final int HOUR = 3; +    public static final int MONTH_DAY = 4; +    public static final int MONTH = 5; +    public static final int YEAR = 6; +    public static final int WEEK_DAY = 7; +    public static final int YEAR_DAY = 8; +    public static final int WEEK_NUM = 9; + +    public static final int SUNDAY = 0; +    public static final int MONDAY = 1; +    public static final int TUESDAY = 2; +    public static final int WEDNESDAY = 3; +    public static final int THURSDAY = 4; +    public static final int FRIDAY = 5; +    public static final int SATURDAY = 6; + +    /** +     * Construct a Time object in the timezone named by the string +     * argument "timezone". The time is initialized to Jan 1, 1970. +     */ +    public Time(String timezone) { +        if (timezone == null) { +            throw new NullPointerException("timezone is null!"); +        } +        this.timezone = timezone; +        this.year = 1970; +        this.monthDay = 1; +        // Set the daylight-saving indicator to the unknown value -1 so that +        // it will be recomputed. +        this.isDst = -1; +    } + +    /** +     * Construct a Time object in the local timezone. The time is initialized to +     * Jan 1, 1970. +     */ +    public Time() { +        this(TimeZone.getDefault().getID()); +    } +     +    /** +     * A copy constructor.  Construct a Time object by copying the given +     * Time object.  No normalization occurs. +     *  +     * @param other +     */ +    public Time(Time other) { +        set(other); +    } + +    /** +     * Ensures the values in each field are in range. For example if the +     * current value of this calendar is March 32, normalize() will convert it +     * to April 1. It also fills in weekDay, yearDay, isDst and gmtoff. +     *  +     * <p> +     * If "ignoreDst" is true, then this method sets the "isDst" field to -1 +     * (the "unknown" value) before normalizing.  It then computes the +     * correct value for "isDst". +     *  +     * <p> +     * See {@link #toMillis(boolean)} for more information about when to +     * use <tt>true</tt> or <tt>false</tt> for "ignoreDst". +     *  +     * @return the UTC milliseconds since the epoch  +     */ +    native public long normalize(boolean ignoreDst); + +    /** +     * Convert this time object so the time represented remains the same, but is +     * instead located in a different timezone. This method automatically calls +     * normalize() in some cases +     */ +    native public void switchTimezone(String timezone); + +    private static final int[] DAYS_PER_MONTH = { 31, 28, 31, 30, 31, 30, 31, +            31, 30, 31, 30, 31 }; + +    /** +     * Return the maximum possible value for the given field given the value of +     * the other fields. Requires that it be normalized for MONTH_DAY and +     * YEAR_DAY. +     */ +    public int getActualMaximum(int field) { +        switch (field) { +        case SECOND: +            return 59; // leap seconds, bah humbug +        case MINUTE: +            return 59; +        case HOUR: +            return 23; +        case MONTH_DAY: { +            int n = DAYS_PER_MONTH[this.month]; +            if (n != 28) { +                return n; +            } else { +                int y = this.year; +                return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 29 : 28; +            } +        } +        case MONTH: +            return 11; +        case YEAR: +            return 2037; +        case WEEK_DAY: +            return 6; +        case YEAR_DAY: { +            int y = this.year; +            // Year days are numbered from 0, so the last one is usually 364. +            return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 365 : 364; +        } +        case WEEK_NUM: +            throw new RuntimeException("WEEK_NUM not implemented"); +        default: +            throw new RuntimeException("bad field=" + field); +        } +    } + +    /** +     * Clears all values, setting the timezone to the given timezone. Sets isDst +     * to a negative value to mean "unknown". +     */ +    public void clear(String timezone) { +        if (timezone == null) { +            throw new NullPointerException("timezone is null!"); +        } +        this.timezone = timezone; +        this.allDay = false; +        this.second = 0; +        this.minute = 0; +        this.hour = 0; +        this.monthDay = 0; +        this.month = 0; +        this.year = 0; +        this.weekDay = 0; +        this.yearDay = 0; +        this.gmtoff = 0; +        this.isDst = -1; +    } + +    /** +     * return a negative number if a is less than b, a positive number if a is +     * greater than b, and 0 if they are equal. +     */ +    native public static int compare(Time a, Time b); + +    /** +     * Print the current value given the format string provided. See man +     * strftime for what means what. The final string must be less than 256 +     * characters. +     */ +    native public String format(String format); + +    /** +     * Return the current time in YYYYMMDDTHHMMSS<tz> format +     */ +    @Override +    native public String toString(); + +    /** +     * Parse a time in the current zone in YYYYMMDDTHHMMSS format. +     */ +    native public void parse(String s); + +    /** +     * Parse a time in RFC 2445 format. Returns whether or not the time is in +     * UTC (ends with Z). +     * +     * @param s the string to parse +     * @return true if the resulting time value is in UTC time +     */ +    public boolean parse2445(String s) { +        if (nativeParse2445(s)) { +            timezone = TIMEZONE_UTC; +            return true; +        } +        return false; +    } + +    native private boolean nativeParse2445(String s); + +    /** +     * Parse a time in RFC 3339 format.  This method also parses simple dates +     * (that is, strings that contain no time or time offset).  If the string +     * contains a time and time offset, then the time offset will be used to +     * convert the time value to UTC. +     * Returns true if the resulting time value is in UTC time. +     * +     * @param s the string to parse +     * @return true if the resulting time value is in UTC time +     */ +     public boolean parse3339(String s) { +         if (nativeParse3339(s)) { +             timezone = TIMEZONE_UTC; +             return true; +         } +         return false; +     } +      +     native private boolean nativeParse3339(String s); + +    /** +     * Returns the timezone string that is currently set for the device. +     */ +    public static String getCurrentTimezone() { +        return TimeZone.getDefault().getID(); +    } + +    /** +     * Sets the time of the given Time object to the current time. +     */ +    native public void setToNow(); + +    /** +     * Converts this time to milliseconds. Suitable for interacting with the +     * standard java libraries. The time is in UTC milliseconds since the epoch. +     * This does an implicit normalization to compute the milliseconds but does +     * <em>not</em> change any of the fields in this Time object.  If you want +     * to normalize the fields in this Time object and also get the milliseconds +     * then use {@link #normalize(boolean)}. +     *  +     * <p> +     * If "ignoreDst" is false, then this method uses the current setting of the +     * "isDst" field and will adjust the returned time if the "isDst" field is +     * wrong for the given time.  See the sample code below for an example of +     * this. +     *  +     * <p> +     * If "ignoreDst" is true, then this method ignores the current setting of +     * the "isDst" field in this Time object and will instead figure out the +     * correct value of "isDst" (as best it can) from the fields in this +     * Time object.  The only case where this method cannot figure out the +     * correct value of the "isDst" field is when the time is inherently +     * ambiguous because it falls in the hour that is repeated when switching +     * from Daylight-Saving Time to Standard Time. +     *  +     * <p> +     * Here is an example where <tt>toMillis(true)</tt> adjusts the time, +     * assuming that DST changes at 2am on Sunday, Nov 4, 2007. +     *  +     * <pre> +     * Time time = new Time(); +     * time.set(2007, 10, 4);  // set the date to Nov 4, 2007, 12am +     * time.normalize();       // this sets isDst = 1 +     * time.monthDay += 1;     // changes the date to Nov 5, 2007, 12am +     * millis = time.toMillis(false);   // millis is Nov 4, 2007, 11pm +     * millis = time.toMillis(true);    // millis is Nov 5, 2007, 12am +     * </pre> +     *  +     * <p> +     * To avoid this problem, use <tt>toMillis(true)</tt> +     * after adding or subtracting days or explicitly setting the "monthDay" +     * field.  On the other hand, if you are adding +     * or subtracting hours or minutes, then you should use +     * <tt>toMillis(false)</tt>. +     *  +     * <p> +     * You should also use <tt>toMillis(false)</tt> if you want +     * to read back the same milliseconds that you set with {@link #set(long)} +     * or {@link #set(Time)} or after parsing a date string. +     */ +    native public long toMillis(boolean ignoreDst); + +    /** +     * Sets the fields in this Time object given the UTC milliseconds.  After +     * this method returns, all the fields are normalized. +     * This also sets the "isDst" field to the correct value. +     *  +     * @param millis the time in UTC milliseconds since the epoch. +     */ +    native public void set(long millis); + +    /** +     * Format according to RFC 2445 DATETIME type. +     *  +     * <p> +     * The same as format("%Y%m%dT%H%M%S"). +     */ +    native public String format2445(); + +    /** +     * Copy the value of that to this Time object. No normalization happens. +     */ +    public void set(Time that) { +        this.timezone = that.timezone; +        this.allDay = that.allDay; +        this.second = that.second; +        this.minute = that.minute; +        this.hour = that.hour; +        this.monthDay = that.monthDay; +        this.month = that.month; +        this.year = that.year; +        this.weekDay = that.weekDay; +        this.yearDay = that.yearDay; +        this.isDst = that.isDst; +        this.gmtoff = that.gmtoff; +    } + +    /** +     * Set the fields. Sets weekDay, yearDay and gmtoff to 0. Call +     * normalize() if you need those. +     */ +    public void set(int second, int minute, int hour, int monthDay, int month, int year) { +        this.allDay = false; +        this.second = second; +        this.minute = minute; +        this.hour = hour; +        this.monthDay = monthDay; +        this.month = month; +        this.year = year; +        this.weekDay = 0; +        this.yearDay = 0; +        this.isDst = -1; +        this.gmtoff = 0; +    } + +    public void set(int monthDay, int month, int year) { +        this.allDay = true; +        this.second = 0; +        this.minute = 0; +        this.hour = 0; +        this.monthDay = monthDay; +        this.month = month; +        this.year = year; +        this.weekDay = 0; +        this.yearDay = 0; +        this.isDst = -1; +        this.gmtoff = 0; +    } + +    public boolean before(Time that) { +        return Time.compare(this, that) < 0; +    } + +    public boolean after(Time that) { +        return Time.compare(this, that) > 0; +    } + +    /** +     * This array is indexed by the weekDay field (SUNDAY=0, MONDAY=1, etc.) +     * and gives a number that can be added to the yearDay to give the +     * closest Thursday yearDay. +     */ +    private static final int[] sThursdayOffset = { -3, 3, 2, 1, 0, -1, -2 }; +         +    /** +     * Computes the week number according to ISO 8601.  The current Time +     * object must already be normalized because this method uses the +     * yearDay and weekDay fields. +     *  +     * In IS0 8601, weeks start on Monday. +     * The first week of the year (week 1) is defined by ISO 8601 as the +     * first week with four or more of its days in the starting year. +     * Or equivalently, the week containing January 4.  Or equivalently, +     * the week with the year's first Thursday in it. +     *  +     * The week number can be calculated by counting Thursdays.  Week N +     * contains the Nth Thursday of the year. +     *    +     * @return the ISO week number. +     */ +    public int getWeekNumber() { +        // Get the year day for the closest Thursday +        int closestThursday = yearDay + sThursdayOffset[weekDay]; + +        // Year days start at 0 +        if (closestThursday >= 0 && closestThursday <= 364) { +            return closestThursday / 7 + 1; +        } +         +        // The week crosses a year boundary. +        Time temp = new Time(this); +        temp.monthDay += sThursdayOffset[weekDay]; +        temp.normalize(true /* ignore isDst */); +        return temp.yearDay / 7 + 1; +    } + +    public String format3339(boolean allDay) { +        if (allDay) { +            return format("%Y-%m-%d"); +        } else if (TIMEZONE_UTC.equals(timezone)) { +            return format("%Y-%m-%dT%H:%M:%S.000Z"); +        } else { +            String base = format("%Y-%m-%dT%H:%M:%S.000"); +            String sign = (gmtoff < 0) ? "-" : "+"; +            int offset = (int)Math.abs(gmtoff); +            int minutes = (offset % 3600) / 60; +            int hours = offset / 3600; +             +            return String.format("%s%s%02d:%02d", base, sign, hours, minutes); +        } +    } +     +    public static boolean isEpoch(Time time) { +        long millis = time.toMillis(true); +        return getJulianDay(millis, 0) == EPOCH_JULIAN_DAY; +    } +     +    /** +     * Computes the Julian day number, given the UTC milliseconds +     * and the offset (in seconds) from UTC.  The Julian day for a given +     * date will be the same for every timezone.  For example, the Julian +     * day for July 1, 2008 is 2454649.  This is the same value no matter +     * what timezone is being used.  The Julian day is useful for testing +     * if two events occur on the same day and for determining the relative +     * time of an event from the present ("yesterday", "3 days ago", etc.). +     *  +     * <p> +     * Use {@link #toMillis(boolean)} to get the milliseconds. +     *  +     * @param millis the time in UTC milliseconds +     * @param gmtoff the offset from UTC in seconds +     * @return the Julian day +     */ +    public static int getJulianDay(long millis, long gmtoff) { +        long offsetMillis = gmtoff * 1000; +        long julianDay = (millis + offsetMillis) / DateUtils.DAY_IN_MILLIS; +        return (int) julianDay + EPOCH_JULIAN_DAY; +    } +     +    /** +     * <p>Sets the time from the given Julian day number, which must be based on +     * the same timezone that is set in this Time object.  The "gmtoff" field +     * need not be initialized because the given Julian day may have a different +     * GMT offset than whatever is currently stored in this Time object anyway. +     * After this method returns all the fields will be normalized and the time +     * will be set to 12am at the beginning of the given Julian day. +     *  +     * <p> +     * The only exception to this is if 12am does not exist for that day because +     * of daylight saving time.  For example, Cairo, Eqypt moves time ahead one +     * hour at 12am on April 25, 2008 and there are a few other places that +     * also change daylight saving time at 12am.  In those cases, the time +     * will be set to 1am. +     *  +     * @param julianDay the Julian day in the timezone for this Time object +     * @return the UTC milliseconds for the beginning of the Julian day +     */ +    public long setJulianDay(int julianDay) { +        // Don't bother with the GMT offset since we don't know the correct +        // value for the given Julian day.  Just get close and then adjust +        // the day. +        long millis = (julianDay - EPOCH_JULIAN_DAY) * DateUtils.DAY_IN_MILLIS; +        set(millis); +         +        // Figure out how close we are to the requested Julian day. +        // We can't be off by more than a day. +        int approximateDay = getJulianDay(millis, gmtoff); +        int diff = julianDay - approximateDay; +        monthDay += diff; +         +        // Set the time to 12am and re-normalize. +        hour = 0; +        minute = 0; +        second = 0; +        millis = normalize(true); +        return millis; +    } +} 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 | 
