summaryrefslogtreecommitdiffstats
path: root/core/java/android/pim
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/pim')
-rw-r--r--core/java/android/pim/ContactsAsyncHelper.java336
-rw-r--r--core/java/android/pim/DateException.java26
-rw-r--r--core/java/android/pim/DateFormat.java493
-rw-r--r--core/java/android/pim/DateUtils.java1408
-rw-r--r--core/java/android/pim/EventRecurrence.java420
-rw-r--r--core/java/android/pim/ICalendar.java643
-rw-r--r--core/java/android/pim/RecurrenceSet.java398
-rw-r--r--core/java/android/pim/Time.java570
-rw-r--r--core/java/android/pim/package.html7
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 &apos;M&apos; 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 -&gt; 9<br/>
+ MM -&gt; 09<br/>
+ MMM -&gt; Sep<br/>
+ MMMM -&gt; 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 -&gt; 7<br/>
+ mm -&gt; 07<br/>
+ mmm -&gt; 007<br/>
+ mmmm -&gt; 0007
+ </p>
+ <p>
+ Examples for April 6, 1970 at 3:23am:<br/>
+ &quot;MM/dd/yy h:mmaa&quot; -&gt; &quot;04/06/70 3:23am&quot<br/>
+ &quot;MMM dd, yyyy h:mmaa&quot; -&gt; &quot;Apr 6, 1970 3:23am&quot<br/>
+ &quot;MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;April 6, 1970 3:23am&quot<br/>
+ &quot;E, MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;Mon, April 6, 1970 3:23am&<br/>
+ &quot;EEEE, MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;Monday, April 6, 1970 3:23am&quot;<br/>
+ &quot;&apos;Best day evar: &apos;M/d/yy&quot; -&gt; &quot;Best day evar: 4/6/70&quot;
+ */
+
+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