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 |