15862a85e17e81866ca82a9905577931947fbd44eMarc Blank/*
25862a85e17e81866ca82a9905577931947fbd44eMarc Blank * Copyright (C) 2010 The Android Open Source Project
35862a85e17e81866ca82a9905577931947fbd44eMarc Blank *
45862a85e17e81866ca82a9905577931947fbd44eMarc Blank * Licensed under the Apache License, Version 2.0 (the "License");
55862a85e17e81866ca82a9905577931947fbd44eMarc Blank * you may not use this file except in compliance with the License.
65862a85e17e81866ca82a9905577931947fbd44eMarc Blank * You may obtain a copy of the License at
75862a85e17e81866ca82a9905577931947fbd44eMarc Blank *
85862a85e17e81866ca82a9905577931947fbd44eMarc Blank *      http://www.apache.org/licenses/LICENSE-2.0
95862a85e17e81866ca82a9905577931947fbd44eMarc Blank *
105862a85e17e81866ca82a9905577931947fbd44eMarc Blank * Unless required by applicable law or agreed to in writing, software
115862a85e17e81866ca82a9905577931947fbd44eMarc Blank * distributed under the License is distributed on an "AS IS" BASIS,
125862a85e17e81866ca82a9905577931947fbd44eMarc Blank * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
135862a85e17e81866ca82a9905577931947fbd44eMarc Blank * See the License for the specific language governing permissions and
145862a85e17e81866ca82a9905577931947fbd44eMarc Blank * limitations under the License.
155862a85e17e81866ca82a9905577931947fbd44eMarc Blank */
165862a85e17e81866ca82a9905577931947fbd44eMarc Blank
175862a85e17e81866ca82a9905577931947fbd44eMarc Blankpackage com.android.exchange.utility;
185862a85e17e81866ca82a9905577931947fbd44eMarc Blank
19c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blankimport android.content.ContentResolver;
20c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blankimport android.content.ContentUris;
2177110d3a646dd691d84abd0b1e083385c1418ac5Marc Blankimport android.content.ContentValues;
225c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blankimport android.content.Context;
23c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blankimport android.content.Entity;
24c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blankimport android.content.Entity.NamedContentValues;
25c8e4352ea6cfa67f15140512e84af8ccede222d2Marc Blankimport android.content.EntityIterator;
26dafc866120dac68fabbddcc9943e3995894c5244Marc Blankimport android.content.res.Resources;
27185060cbeb39dc4539fbc0c72a865d8ec8d12979Jay Shraunerimport android.database.Cursor;
2877110d3a646dd691d84abd0b1e083385c1418ac5Marc Blankimport android.net.Uri;
29693ed7fdd5a7ec7af87d105b76267c78a8acc3dbRoboErikimport android.provider.CalendarContract.Attendees;
30693ed7fdd5a7ec7af87d105b76267c78a8acc3dbRoboErikimport android.provider.CalendarContract.Calendars;
31693ed7fdd5a7ec7af87d105b76267c78a8acc3dbRoboErikimport android.provider.CalendarContract.Events;
32693ed7fdd5a7ec7af87d105b76267c78a8acc3dbRoboErikimport android.provider.CalendarContract.EventsEntity;
33efae936b117c9e4f3056d52fdbfe4d3f261483e5Marc Blankimport android.text.TextUtils;
3477110d3a646dd691d84abd0b1e083385c1418ac5Marc Blankimport android.text.format.Time;
356137d3f2ce68db51926a5e33bf1f57e49bcf8a31Doug Zongkerimport android.util.Base64;
365862a85e17e81866ca82a9905577931947fbd44eMarc Blank
37976e98bfc2dd7f782fc92cc4242f9dcebc2b6858mindypimport com.android.calendarcommon2.DateException;
38976e98bfc2dd7f782fc92cc4242f9dcebc2b6858mindypimport com.android.calendarcommon2.Duration;
39f352bc9f29cacc61b195069e48d5c8b868660694Marc Blankimport com.android.emailcommon.Logging;
4060df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.mail.Address;
4160df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.Account;
4260df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.EmailContent;
4360df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.EmailContent.Attachment;
4460df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.EmailContent.Message;
4560df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.Mailbox;
4660df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.service.AccountServiceProxy;
4760df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.utility.Utility;
4860df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.Eas;
4960df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.R;
5060df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.adapter.Serializer;
5160df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.adapter.Tags;
52942b7d73f2f5b3d6c651e39463e615fe6902a910Scott Kennedyimport com.android.mail.utils.LogUtils;
5360df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.google.common.annotations.VisibleForTesting;
5460df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank
5514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blankimport java.io.IOException;
56dafc866120dac68fabbddcc9943e3995894c5244Marc Blankimport java.text.DateFormat;
572f369a47e14916a34f49c79c0a246a2e3ac3072fJay Shraunerimport java.text.ParseException;
58c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blankimport java.util.ArrayList;
595862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.Calendar;
605862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.Date;
615862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.GregorianCalendar;
625862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.HashMap;
635862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.TimeZone;
645862a85e17e81866ca82a9905577931947fbd44eMarc Blank
655862a85e17e81866ca82a9905577931947fbd44eMarc Blankpublic class CalendarUtilities {
6604c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik
675862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // NOTE: Most definitions in this class are have package visibility for testing purposes
68110837ebff288a75f9bda067c38e2c46797d99b5Alon Albert    private static final String TAG = Eas.LOG_TAG;
695862a85e17e81866ca82a9905577931947fbd44eMarc Blank
705862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Time related convenience constants, in milliseconds
715862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int SECONDS = 1000;
725862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MINUTES = SECONDS*60;
735862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int HOURS = MINUTES*60;
74377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static final long DAYS = HOURS*24;
755862a85e17e81866ca82a9905577931947fbd44eMarc Blank
762c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // We want to find a time zone whose DST info is accurate to one minute
772c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static final int STANDARD_DST_PRECISION = MINUTES;
782c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // If we can't find one, we'll try a more lenient standard (this is better than guessing a
792c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // time zone, which is what we otherwise do).  Note that this specifically addresses an issue
802c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // seen in some time zones sent by MS Exchange in which the start and end hour differ
812c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // for no apparent reason
822c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static final int LENIENT_DST_PRECISION = 4*HOURS;
832c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank
8404c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik    private static final String SYNC_VERSION = Events.SYNC_DATA4;
855862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // NOTE All Microsoft data structures are little endian
865862a85e17e81866ca82a9905577931947fbd44eMarc Blank
875862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // The following constants relate to standard Microsoft data sizes
885862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx
895862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_LONG_SIZE = 4;
905862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_WCHAR_SIZE = 2;
915862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_WORD_SIZE = 2;
925862a85e17e81866ca82a9905577931947fbd44eMarc Blank
935862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // The following constants relate to Microsoft's SYSTEMTIME structure
945862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4
955862a85e17e81866ca82a9905577931947fbd44eMarc Blank
965862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE;
975862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE;
985862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE;
995862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE;
1005862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE;
1015862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE;
1025862a85e17e81866ca82a9905577931947fbd44eMarc Blank    //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE;
1035862a85e17e81866ca82a9905577931947fbd44eMarc Blank    //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE;
1045862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE;
1055862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1065862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure
1075862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx
108bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank    static final int MSFT_TIME_ZONE_STRING_SIZE = 32;
109bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank
1105862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0;
1115862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET =
1125862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE;
1135862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET =
114bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank        MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*MSFT_TIME_ZONE_STRING_SIZE);
1155862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET =
1165862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
1175862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET =
1185862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE;
1195862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET =
120bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank        MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*MSFT_TIME_ZONE_STRING_SIZE);
1215862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET =
1225862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
1235862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_SIZE =
1245862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE;
1255862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1265862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // TimeZone cache; we parse/decode as little as possible, because the process is quite slow
1275862a85e17e81866ca82a9905577931947fbd44eMarc Blank    private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>();
128377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    // TZI string cache; we keep around our encoded TimeZoneInformation strings
129377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    private static HashMap<TimeZone, String> sTziStringCache = new HashMap<TimeZone, String>();
1305862a85e17e81866ca82a9905577931947fbd44eMarc Blank
131270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
132937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    // Default, Popup
13380a8e57ce2dc2695ed6f35599d326090e4ad9faeRoboErik    private static final String ALLOWED_REMINDER_TYPES = "0,1";
134937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    // None, required, optional
135937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    private static final String ALLOWED_ATTENDEE_TYPES = "0,1,2";
136937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    // Busy, free, tentative
137937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    private static final String ALLOWED_AVAILABILITIES = "0,1,2";
138270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank
1395862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // There is no type 4 (thus, the "")
1405862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final String[] sTypeToFreq =
1415862a85e17e81866ca82a9905577931947fbd44eMarc Blank        new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"};
1425862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1435862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final String[] sDayTokens =
1445862a85e17e81866ca82a9905577931947fbd44eMarc Blank        new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
1455862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1465862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final String[] sTwoCharacterNumbers =
1475862a85e17e81866ca82a9905577931947fbd44eMarc Blank        new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"};
1485862a85e17e81866ca82a9905577931947fbd44eMarc Blank
149f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    // Bits used in EAS recurrences for days of the week
150f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_SUNDAY = 1<<0;
151f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_MONDAY = 1<<1;
152f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_TUESDAY = 1<<2;
153f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_WEDNESDAY = 1<<3;
154f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_THURSDAY = 1<<4;
155f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_FRIDAY = 1<<5;
156f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_SATURDAY = 1<<6;
157f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_WEEKDAYS =
158f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        EAS_MONDAY | EAS_TUESDAY | EAS_WEDNESDAY | EAS_THURSDAY | EAS_FRIDAY;
159f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_WEEKENDS = EAS_SATURDAY | EAS_SUNDAY;
160f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank
161377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR);
162377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT");
163377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
164bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    private static final String ICALENDAR_ATTENDEE = "ATTENDEE;ROLE=REQ-PARTICIPANT";
165bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_CANCEL = ICALENDAR_ATTENDEE;
166bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_INVITE =
167bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=NEEDS-ACTION;RSVP=TRUE";
168bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_ACCEPT =
169bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=ACCEPTED";
170bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_DECLINE =
171bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=DECLINED";
172bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_TENTATIVE =
173bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=TENTATIVE";
174c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
175e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // Note that these constants apply to Calendar items
176e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // For future reference: MeetingRequest data can also include free/busy information, but the
177e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // constants for these four options in MeetingRequest data have different values!
178e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // See [MS-ASCAL] 2.2.2.8 for Calendar BusyStatus
179e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // See [MS-EMAIL] 2.2.2.34 for MeetingRequest BusyStatus
180e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_FREE = 0;
181e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_TENTATIVE = 1;
182e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_BUSY = 2;
183e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_OUT_OF_OFFICE = 3;
184dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank
18565d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    // Note that these constants apply to Calendar items, and are used in EAS 14+
18665d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    // See [MS-ASCAL] 2.2.2.22 for Calendar ResponseType
18765d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_NONE = 0;
18865d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_ORGANIZER = 1;
18965d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_TENTATIVE = 2;
19065d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_ACCEPTED = 3;
19165d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_DECLINED = 4;
19265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_NOT_RESPONDED = 5;
19365d022dc43e4461e86fd7bc143591f542b07428bMarc Blank
1945862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Return a 4-byte long from a byte array (little endian)
1955862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static int getLong(byte[] bytes, int offset) {
1965862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) |
1975862a85e17e81866ca82a9905577931947fbd44eMarc Blank        ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24);
1985862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
1995862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2005862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Put a 4-byte long into a byte array (little endian)
2015862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static void setLong(byte[] bytes, int offset, int value) {
2025862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) (value & 0xFF);
2035862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) ((value >> 8) & 0xFF);
2045862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) ((value >> 16) & 0xFF);
2055862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset] = (byte) ((value >> 24) & 0xFF);
2065862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2075862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2085862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Return a 2-byte word from a byte array (little endian)
2095862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static int getWord(byte[] bytes, int offset) {
2105862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8);
2115862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2125862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2135862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Put a 2-byte word into a byte array (little endian)
2145862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static void setWord(byte[] bytes, int offset, int value) {
2155862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) (value & 0xFF);
2165862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset] = (byte) ((value >> 8) & 0xFF);
2175862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2185862a85e17e81866ca82a9905577931947fbd44eMarc Blank
219bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank    static String getString(byte[] bytes, int offset, int size) {
220bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank        StringBuilder sb = new StringBuilder();
221bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank        for (int i = 0; i < size; i++) {
222bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank            int ch = bytes[offset + i];
223bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank            if (ch == 0) {
224bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                break;
225bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank            } else {
226bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                sb.append((char)ch);
227bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank            }
228bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank        }
229bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank        return sb.toString();
230bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank    }
231bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank
2325862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Internal structure for storing a time zone date from a SYSTEMTIME structure
2335862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // This date represents either the start or the end time for DST
2345862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static class TimeZoneDate {
2355862a85e17e81866ca82a9905577931947fbd44eMarc Blank        String year;
2365862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int month;
2375862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int dayOfWeek;
2385862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int day;
2395862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int time;
2405862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int hour;
2415862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int minute;
2425862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2435862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2444868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank    @VisibleForTesting
2454868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank    static void clearTimeZoneCache() {
2464868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank        sTimeZoneCache.clear();
2474868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank    }
2484868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank
249820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static void putRuleIntoTimeZoneInformation(byte[] bytes, int offset, RRule rrule, int hour,
250820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            int minute) {
251820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // MSFT months are 1 based, same as RRule
252820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, rrule.month);
253820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // MSFT day of week starts w/ Sunday = 0; RRule starts w/ Sunday = 1
254820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, rrule.dayOfWeek - 1);
255820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // 5 means "last" in MSFT land; for RRule, it's -1
256820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, rrule.week < 0 ? 5 : rrule.week);
257820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Turn hours/minutes into ms from midnight (per TimeZone)
258820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, hour);
259820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, minute);
260820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
261820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
26210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    // Write a transition time into SYSTEMTIME data (via an offset into a byte array)
26310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    static void putTransitionMillisIntoSystemTime(byte[] bytes, int offset, long millis) {
264377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
265377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // Round to the next highest minute; we always write seconds as zero
266377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        cal.setTimeInMillis(millis + 30*SECONDS);
267377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
268377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // MSFT months are 1 based; TimeZone is 0 based
269377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, cal.get(Calendar.MONTH) + 1);
270377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
271377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, cal.get(Calendar.DAY_OF_WEEK) - 1);
272377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
273377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // Get the "day" in TimeZone format
274377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        int wom = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
275377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // 5 means "last" in MSFT land; for TimeZone, it's -1
276377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, wom < 0 ? 5 : wom);
277377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
278377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // Turn hours/minutes into ms from midnight (per TimeZone)
27910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, getTrueTransitionHour(cal));
28010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, getTrueTransitionMinute(cal));
281377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     }
282377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
2835862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset
2845862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) {
2855862a85e17e81866ca82a9905577931947fbd44eMarc Blank        TimeZoneDate tzd = new TimeZoneDate();
2865862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2875862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT year is an int; TimeZone is a String
2885862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR);
2895862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.year = Integer.toString(num);
2905862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2915862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT month = 0 means no daylight time
2925862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT months are 1 based; TimeZone is 0 based
2935862a85e17e81866ca82a9905577931947fbd44eMarc Blank        num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH);
2945862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (num == 0) {
2955862a85e17e81866ca82a9905577931947fbd44eMarc Blank            return null;
2965862a85e17e81866ca82a9905577931947fbd44eMarc Blank        } else {
2975862a85e17e81866ca82a9905577931947fbd44eMarc Blank            tzd.month = num -1;
2985862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
2995862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3005862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
3015862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1;
3025862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3035862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Get the "day" in TimeZone format
3045862a85e17e81866ca82a9905577931947fbd44eMarc Blank        num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY);
3055862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // 5 means "last" in MSFT land; for TimeZone, it's -1
3065862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (num == 5) {
3075862a85e17e81866ca82a9905577931947fbd44eMarc Blank            tzd.day = -1;
3085862a85e17e81866ca82a9905577931947fbd44eMarc Blank        } else {
3095862a85e17e81866ca82a9905577931947fbd44eMarc Blank            tzd.day = num;
3105862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
3115862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3125862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Turn hours/minutes into ms from midnight (per TimeZone)
3135862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR);
3145862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.hour = hour;
3155862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE);
3165862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.minute = minute;
3175862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.time = (hour*HOURS) + (minute*MINUTES);
3185862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3195862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return tzd;
3205862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
3215862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3225862a85e17e81866ca82a9905577931947fbd44eMarc Blank    /**
3235862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * Build a GregorianCalendar, based on a time zone and TimeZoneDate.
3245862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @param timeZone the time zone we're checking
3255862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @param tzd the TimeZoneDate we're interested in
3265862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @return a GregorianCalendar with the given time zone and date
3275862a85e17e81866ca82a9905577931947fbd44eMarc Blank     */
32879268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static long getMillisAtTimeZoneDateTransition(TimeZone timeZone, TimeZoneDate tzd) {
3295862a85e17e81866ca82a9905577931947fbd44eMarc Blank        GregorianCalendar testCalendar = new GregorianCalendar(timeZone);
330377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        testCalendar.set(GregorianCalendar.YEAR, sCurrentYear);
3315862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.MONTH, tzd.month);
3325862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek);
3335862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day);
3345862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour);
3355862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.MINUTE, tzd.minute);
33679268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        testCalendar.set(GregorianCalendar.SECOND, 0);
33779268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        return testCalendar.getTimeInMillis();
3385862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
3395862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3405862a85e17e81866ca82a9905577931947fbd44eMarc Blank    /**
341820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Return a GregorianCalendar representing the first standard/daylight transition between a
342820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * start time and an end time in the given time zone
343820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz a TimeZone the time zone in which we're looking for transitions
344377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param startTime the start time for the test
345377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param endTime the end time for the test
346377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param startInDaylightTime whether daylight time is in effect at the startTime
347820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return a GregorianCalendar representing the transition or null if none
348377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
34979268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static GregorianCalendar findTransitionDate(TimeZone tz, long startTime,
35010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            long endTime, boolean startInDaylightTime) {
351377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        long startingEndTime = endTime;
352377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        Date date = null;
35310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank
354820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll keep splitting the difference until we're within a minute
355377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        while ((endTime - startTime) > MINUTES) {
356377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            long checkTime = ((startTime + endTime) / 2) + 1;
357377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            date = new Date(checkTime);
35810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            boolean inDaylightTime = tz.inDaylightTime(date);
35910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            if (inDaylightTime != startInDaylightTime) {
360377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                endTime = checkTime;
361377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            } else {
362377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                startTime = checkTime;
363377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
364377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
365820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
366820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // If these are the same, we're really messed up; return null
367377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (endTime == startingEndTime) {
368820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return null;
369377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
370820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
37110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        // Set up our calendar and return it
372820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar calendar = new GregorianCalendar(tz);
37310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        calendar.setTimeInMillis(startTime);
374820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return calendar;
375377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
376377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
377377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
378377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Return a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
379377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * that might be found in an Event; use cached result, if possible
380377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param tz the TimeZone
381377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
382377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
383377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static public String timeZoneToTziString(TimeZone tz) {
384377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        String tziString = sTziStringCache.get(tz);
385377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (tziString != null) {
386377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            if (Eas.USER_LOG) {
387bb0141b49e7eff978fa445249dc888461ea581e3Martin Hibdon                LogUtils.d(TAG, "TZI string for " + tz.getDisplayName() +
388385a0be662509754e687bcfa9813208b050bf951Marc Blank                        " found in cache.");
389377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
390377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            return tziString;
391377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
392377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        tziString = timeZoneToTziStringImpl(tz);
393377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sTziStringCache.put(tz, tziString);
394377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return tziString;
395377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
396377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
397377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
398820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * A class for storing RRULE information.  The RRULE members can be accessed individually or
399820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * an RRULE string can be created with toString()
400820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
401820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static class RRule {
402820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        static final int RRULE_NONE = 0;
403820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        static final int RRULE_DAY_WEEK = 1;
404820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        static final int RRULE_DATE = 2;
405820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
406820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int type;
407820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int dayOfWeek;
408820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int week;
409820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int month;
410820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int date;
411820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
412820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        /**
413820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * Create an RRULE based on month and date
414820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _month the month (1 = JAN, 12 = DEC)
415820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _date the date in the month (1-31)
416820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         */
417820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule(int _month, int _date) {
418820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            type = RRULE_DATE;
419820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            month = _month;
420820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            date = _date;
421820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
422820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
423820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        /**
424820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * Create an RRULE based on month, day of week, and week #
425820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _month the month (1 = JAN, 12 = DEC)
426820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _dayOfWeek the day of the week (1 = SU, 7 = SA)
427820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _week the week in the month (1-5 or -1 for last)
428820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         */
429820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule(int _month, int _dayOfWeek, int _week) {
430820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            type = RRULE_DAY_WEEK;
431820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            month = _month;
432820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            dayOfWeek = _dayOfWeek;
433820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            week = _week;
434820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
435820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
436820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        @Override
437820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        public String toString() {
438820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (type == RRULE_DAY_WEEK) {
439820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return "FREQ=YEARLY;BYMONTH=" + month + ";BYDAY=" + week +
440820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    sDayTokens[dayOfWeek - 1];
441820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
442820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return "FREQ=YEARLY;BYMONTH=" + month + ";BYMONTHDAY=" + date;
443820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
444820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank       }
445820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
446820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
447820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
448820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Generate an RRULE string for an array of GregorianCalendars, if possible.  For now, we are
449820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * only looking for rules based on the same date in a month or a specific instance of a day of
450820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the week in a month (e.g. 2nd Tuesday or last Friday).  Indeed, these are the only kinds of
451820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * rules used in the current tzinfo database.
452820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param calendars an array of GregorianCalendar, set to a series of transition times in
453820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * consecutive years starting with the current year
454820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return an RRULE or null if none could be inferred from the calendars
455820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
45679268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static RRule inferRRuleFromCalendars(GregorianCalendar[] calendars) {
457820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Let's see if we can make a rule about these
458820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar calendar = calendars[0];
459820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (calendar == null) return null;
460820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int month = calendar.get(Calendar.MONTH);
461820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int date = calendar.get(Calendar.DAY_OF_MONTH);
462820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
463820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int week = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
464820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int maxWeek = calendar.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
465820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        boolean dateRule = false;
466820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        boolean dayOfWeekRule = false;
467820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        for (int i = 1; i < calendars.length; i++) {
468820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar cal = calendars[i];
469820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal == null) return null;
470820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // If it's not the same month, there's no rule
471820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal.get(Calendar.MONTH) != month) {
472820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return null;
473820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (dayOfWeek == cal.get(Calendar.DAY_OF_WEEK)) {
474820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Ok, it seems to be the same day of the week
475820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (dateRule) {
476820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    return null;
477820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
478820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                dayOfWeekRule = true;
479820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                int thisWeek = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
480820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (week != thisWeek) {
481820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    if (week < 0 || week == maxWeek) {
482820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        int thisMaxWeek = cal.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
483820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        if (thisWeek == thisMaxWeek) {
484820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            // We'll use -1 (i.e. last) week
485820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            week = -1;
486820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            continue;
487820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        }
488820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    }
489820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    return null;
490820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
491820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (date == cal.get(Calendar.DAY_OF_MONTH)) {
492820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Maybe the same day of the month?
493820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (dayOfWeekRule) {
494820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    return null;
495820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
496820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                dateRule = true;
497820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
498820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return null;
499820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
500820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
501820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
502820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (dateRule) {
503820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return new RRule(month + 1, date);
504820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
505820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // sDayTokens is 0 based (SU = 0); Calendar days of week are 1 based (SU = 1)
506820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // iCalendar months are 1 based; Calendar months are 0 based
507820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // So we adjust these when building the string
508820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return new RRule(month + 1, dayOfWeek, week);
509820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
510820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
511820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
512820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Generate an rfc2445 utcOffset from minutes offset from GMT
513820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * These look like +0800 or -0100
514820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param offsetMinutes minutes offset from GMT (east is positive, west is negative
515820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return a utcOffset
516820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
51779268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static String utcOffsetString(int offsetMinutes) {
518820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        StringBuilder sb = new StringBuilder();
519820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int hours = offsetMinutes / 60;
520820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hours < 0) {
521820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('-');
522820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            hours = 0 - hours;
523820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        } else {
524820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('+');
525820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
526820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int minutes = offsetMinutes % 60;
527820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hours < 10) {
528820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('0');
529820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
530820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(hours);
531820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (minutes < 10) {
532820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('0');
533820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
534820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(minutes);
535820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return sb.toString();
536820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
537820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
538820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
539820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Fill the passed in GregorianCalendars arrays with DST transition information for this and
540820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the following years (based on the length of the arrays)
541820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz the time zone
542820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param toDaylightCalendars an array of GregorianCalendars, one for each year, representing
543820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the transition to daylight time
544820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param toStandardCalendars an array of GregorianCalendars, one for each year, representing
545820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the transition to standard time
546820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return true if transitions could be found for all years, false otherwise
547820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
548820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static boolean getDSTCalendars(TimeZone tz, GregorianCalendar[] toDaylightCalendars,
549820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar[] toStandardCalendars) {
550820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll use the length of the arrays to determine how many years to check
551820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int maxYears = toDaylightCalendars.length;
552820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (toStandardCalendars.length != maxYears) {
553820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return false;
554820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
555820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Get the transitions for this year and the next few years
556820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        for (int i = 0; i < maxYears; i++) {
557820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar cal = new GregorianCalendar(tz);
558820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            cal.set(sCurrentYear + i, Calendar.JANUARY, 1, 0, 0, 0);
559820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            long startTime = cal.getTimeInMillis();
560820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Calculate end of year; no need to be insanely precise
561820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            long endOfYearTime = startTime + (365*DAYS) + (DAYS>>2);
562820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            Date date = new Date(startTime);
563820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            boolean startInDaylightTime = tz.inDaylightTime(date);
564820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Find the first transition, and store
565820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            cal = findTransitionDate(tz, startTime, endOfYearTime, startInDaylightTime);
566820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal == null) {
567820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return false;
568820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (startInDaylightTime) {
569820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toStandardCalendars[i] = cal;
570820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
571820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toDaylightCalendars[i] = cal;
572820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
573820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Find the second transition, and store
574820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            cal = findTransitionDate(tz, startTime, endOfYearTime, !startInDaylightTime);
575820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal == null) {
576820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return false;
577820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (startInDaylightTime) {
578820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toDaylightCalendars[i] = cal;
579820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
580820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toStandardCalendars[i] = cal;
581820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
582820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
583820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return true;
584820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
585820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
586820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
587820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Write out the STANDARD block of VTIMEZONE and end the VTIMEZONE
588820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param writer the SimpleIcsWriter we're using
589820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz the time zone
590820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param offsetString the offset string in VTIMEZONE format (e.g. +0800)
591820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @throws IOException
592820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
593820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static private void writeNoDST(SimpleIcsWriter writer, TimeZone tz, String offsetString)
594820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            throws IOException {
595820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "STANDARD");
596820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETFROM", offsetString);
597820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETTO", offsetString);
598820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Might as well use start of epoch for start date
599820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("DTSTART", millisToEasDateTime(0L));
600820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "STANDARD");
601820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "VTIMEZONE");
602820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
603820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
604820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /** Write a VTIMEZONE block for a given TimeZone into a SimpleIcsWriter
605820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz the TimeZone to be used in the conversion
606820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param writer the SimpleIcsWriter to be used
607820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @throws IOException
608820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
60979268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static void timeZoneToVTimezone(TimeZone tz, SimpleIcsWriter writer)
610820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            throws IOException {
611820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll use these regardless of whether there's DST in this time zone or not
612820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int rawOffsetMinutes = tz.getRawOffset() / MINUTES;
613820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        String standardOffsetString = utcOffsetString(rawOffsetMinutes);
614820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
615820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Preamble for all of our VTIMEZONEs
616820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "VTIMEZONE");
617820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZID", tz.getID());
618820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("X-LIC-LOCATION", tz.getDisplayName());
619820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
620820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Simplest case is no daylight time
621820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (!tz.useDaylightTime()) {
622820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writeNoDST(writer, tz, standardOffsetString);
623820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return;
624820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
625820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
626820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int maxYears = 3;
627820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[maxYears];
628820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar[] toStandardCalendars = new GregorianCalendar[maxYears];
629820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (!getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) {
630820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writeNoDST(writer, tz, standardOffsetString);
631820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return;
632820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
633820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Try to find a rule to cover these yeras
634820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars);
635820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule standardRule = inferRRuleFromCalendars(toStandardCalendars);
636820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        String daylightOffsetString =
637820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            utcOffsetString(rawOffsetMinutes + (tz.getDSTSavings() / MINUTES));
638820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll use RRULE's if we found both
639820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Otherwise we write the first as DTSTART and the others as RDATE
640820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        boolean hasRule = daylightRule != null && standardRule != null;
641820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
642820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Write the DAYLIGHT block
643820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "DAYLIGHT");
644820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETFROM", standardOffsetString);
645820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETTO", daylightOffsetString);
646820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("DTSTART",
64710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                transitionMillisToVCalendarTime(
64810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        toDaylightCalendars[0].getTimeInMillis(), tz, true));
649820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hasRule) {
650820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writer.writeTag("RRULE", daylightRule.toString());
651820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        } else {
652820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            for (int i = 1; i < maxYears; i++) {
65310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                writer.writeTag("RDATE", transitionMillisToVCalendarTime(
654820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        toDaylightCalendars[i].getTimeInMillis(), tz, true));
655820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
656820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
657820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "DAYLIGHT");
658820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Write the STANDARD block
659820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "STANDARD");
660820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETFROM", daylightOffsetString);
661820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETTO", standardOffsetString);
662820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("DTSTART",
66310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                transitionMillisToVCalendarTime(
66410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        toStandardCalendars[0].getTimeInMillis(), tz, false));
665820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hasRule) {
666820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writer.writeTag("RRULE", standardRule.toString());
667820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        } else {
668820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            for (int i = 1; i < maxYears; i++) {
66910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                writer.writeTag("RDATE", transitionMillisToVCalendarTime(
670820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        toStandardCalendars[i].getTimeInMillis(), tz, true));
671820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
672820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
673820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "STANDARD");
674820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // And we're done
675820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "VTIMEZONE");
676820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
677820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
678820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
679820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Find the next transition to occur (i.e. after the current date/time)
680820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param transitions calendars representing transitions to/from DST
681820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return millis for the first transition after the current date/time
682820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
68379268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static long findNextTransition(long startingMillis, GregorianCalendar[] transitions) {
684820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        for (GregorianCalendar transition: transitions) {
685820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            long transitionMillis = transition.getTimeInMillis();
686820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (transitionMillis > startingMillis) {
687820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return transitionMillis;
688820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
689820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
690820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return 0;
691820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
692820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
693820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
694377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Calculate the Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
695377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * that might be found in an Event.  Since the internal representation of the TimeZone is hidden
696377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * from us we'll find the DST transitions and build the structure from that information
697377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param tz the TimeZone
698377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
699377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
70079268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static String timeZoneToTziStringImpl(TimeZone tz) {
701377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        String tziString;
702377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE];
703377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        int standardBias = - tz.getRawOffset();
704377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        standardBias /= 60*SECONDS;
705377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias);
706820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // If this time zone has daylight savings time, we need to do more work
707377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (tz.useDaylightTime()) {
708820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[3];
709820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar[] toStandardCalendars = new GregorianCalendar[3];
710820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // See if we can get transitions for a few years; if not, we can't generate DST info
711820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // for this time zone
712820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) {
713820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Try to find a rule to cover these years
714820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars);
715820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                RRule standardRule = inferRRuleFromCalendars(toStandardCalendars);
716820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if ((daylightRule != null) && (daylightRule.type == RRule.RRULE_DAY_WEEK) &&
717820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        (standardRule != null) && (standardRule.type == RRule.RRULE_DAY_WEEK)) {
718820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // We need both rules and they have to be DAY/WEEK type
719820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // Write month, day of week, week, hour, minute
720820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET,
721820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            standardRule,
72210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionHour(toStandardCalendars[0]),
72310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionMinute(toStandardCalendars[0]));
724820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET,
725820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            daylightRule,
72610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionHour(toDaylightCalendars[0]),
72710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionMinute(toDaylightCalendars[0]));
728820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                } else {
729820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // If there's no rule, we'll use the first transition to standard/to daylight
730820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // And indicate that it's just for this year...
731820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    long now = System.currentTimeMillis();
732820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    long standardTransition = findNextTransition(now, toStandardCalendars);
733820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    long daylightTransition = findNextTransition(now, toDaylightCalendars);
734820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // If we can't find transitions, we can't do DST
735820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    if (standardTransition != 0 && daylightTransition != 0) {
73610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        putTransitionMillisIntoSystemTime(tziBytes,
73710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                                MSFT_TIME_ZONE_STANDARD_DATE_OFFSET, standardTransition);
73810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        putTransitionMillisIntoSystemTime(tziBytes,
73910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                                MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET, daylightTransition);
740820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    }
741820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
742377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
743820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            int dstOffset = tz.getDSTSavings();
744820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES);
745377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
7467e85a8d17b2b0c80bd04306db5a698ac3b91deaaMakoto Onuki        byte[] tziEncodedBytes = Base64.encode(tziBytes, Base64.NO_WRAP);
747377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        tziString = new String(tziEncodedBytes);
748377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return tziString;
749377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
750377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
751377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
7525862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * Given a String as directly read from EAS, returns a TimeZone corresponding to that String
7535862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @param timeZoneString the String read from the server
7542c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * @param precision the number of milliseconds of precision in TimeZone determination
7555862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @return the TimeZone, or TimeZone.getDefault() if not found
7565862a85e17e81866ca82a9905577931947fbd44eMarc Blank     */
7572c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    @VisibleForTesting
7582c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static TimeZone tziStringToTimeZone(String timeZoneString, int precision) {
7595862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // If we have this time zone cached, use that value and return
7605862a85e17e81866ca82a9905577931947fbd44eMarc Blank        TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
7615862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (timeZone != null) {
7625862a85e17e81866ca82a9905577931947fbd44eMarc Blank            if (Eas.USER_LOG) {
763bb0141b49e7eff978fa445249dc888461ea581e3Martin Hibdon                LogUtils.d(TAG, " Using cached TimeZone " + timeZone.getID());
764377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
765377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        } else {
7662c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank            timeZone = tziStringToTimeZoneImpl(timeZoneString, precision);
767377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            if (timeZone == null) {
768377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                // If we don't find a match, we just return the current TimeZone.  In theory, this
769377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                // shouldn't be happening...
770bb0141b49e7eff978fa445249dc888461ea581e3Martin Hibdon                LogUtils.d(TAG, "TimeZone not found using default: " + timeZoneString);
771377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                timeZone = TimeZone.getDefault();
7725862a85e17e81866ca82a9905577931947fbd44eMarc Blank            }
773377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            sTimeZoneCache.put(timeZoneString, timeZone);
7745862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
775377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return timeZone;
776377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
7775862a85e17e81866ca82a9905577931947fbd44eMarc Blank
778377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
7792c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * The standard entry to EAS time zone conversion, using one minute as the precision
7802c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     */
7812c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static public TimeZone tziStringToTimeZone(String timeZoneString) {
7822c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank        return tziStringToTimeZone(timeZoneString, MINUTES);
7832c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    }
7842c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank
785e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank    static private boolean hasTimeZoneId(String[] timeZoneIds, String id) {
786e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank    for (String timeZoneId: timeZoneIds) {
787e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank            if (id.equals(timeZoneId)) {
788e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank                return true;
789e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank            }
790e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        }
791e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        return false;
792e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank    }
793e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank
7942c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    /**
795377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Given a String as directly read from EAS, tries to find a TimeZone in the database of all
7962c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * time zones that corresponds to that String.  If the test time zone string includes DST and
7972c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * we don't find a match, and we're using standard precision, we try again with lenient
7982c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * precision, which is a bit better than guessing
799377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param timeZoneString the String read from the server
80079268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank     * @return the TimeZone, or null if not found
801377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
8022c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static TimeZone tziStringToTimeZoneImpl(String timeZoneString, int precision) {
803377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        TimeZone timeZone = null;
8045862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // First, we need to decode the base64 string
8057e85a8d17b2b0c80bd04306db5a698ac3b91deaaMakoto Onuki        byte[] timeZoneBytes = Base64.decode(timeZoneString, Base64.DEFAULT);
8065862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8075862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms
8085862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // but EAS gives us minutes, so do the conversion.  Note that EAS is the bias that's added
8095862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so
8105862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // we need to change the sign
8115862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES;
8125862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8135862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return
8145862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // the default time zone
8155862a85e17e81866ca82a9905577931947fbd44eMarc Blank        String[] zoneIds = TimeZone.getAvailableIDs(bias);
8165862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (zoneIds.length > 0) {
8175862a85e17e81866ca82a9905577931947fbd44eMarc Blank            // Try to find an existing TimeZone from the data provided by EAS
8185862a85e17e81866ca82a9905577931947fbd44eMarc Blank            // We start by pulling out the date that standard time begins
8195862a85e17e81866ca82a9905577931947fbd44eMarc Blank            TimeZoneDate dstEnd =
8205862a85e17e81866ca82a9905577931947fbd44eMarc Blank                getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET);
8215862a85e17e81866ca82a9905577931947fbd44eMarc Blank            if (dstEnd == null) {
8225bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // If the default time zone is a match
8235bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                TimeZone defaultTimeZone = TimeZone.getDefault();
8245bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                if (!defaultTimeZone.useDaylightTime() &&
825e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank                        hasTimeZoneId(zoneIds, defaultTimeZone.getID())) {
8265bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    if (Eas.USER_LOG) {
827bb0141b49e7eff978fa445249dc888461ea581e3Martin Hibdon                        LogUtils.d(TAG, "TimeZone without DST found to be default: " +
8285bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                                defaultTimeZone.getID());
8295bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    }
8305bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    return defaultTimeZone;
8315bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                }
8325862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // In this case, there is no daylight savings time, so the only interesting data
8335bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // for possible matches is the offset and DST availability; we'll take the first
8345bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // match for those
8355bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                for (String zoneId: zoneIds) {
8365bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    timeZone = TimeZone.getTimeZone(zoneId);
8375bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    if (!timeZone.useDaylightTime()) {
8385bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        if (Eas.USER_LOG) {
839bb0141b49e7eff978fa445249dc888461ea581e3Martin Hibdon                            LogUtils.d(TAG, "TimeZone without DST found by offset: " +
8405bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                                    timeZone.getID());
8415bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        }
8425bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        return timeZone;
8435bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    }
8445862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
8455bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // None found, return null
8465bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                return null;
8475862a85e17e81866ca82a9905577931947fbd44eMarc Blank            } else {
848377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes,
8495862a85e17e81866ca82a9905577931947fbd44eMarc Blank                        MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET);
8505862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // See comment above for bias...
8515862a85e17e81866ca82a9905577931947fbd44eMarc Blank                long dstSavings =
852377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                    -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * MINUTES;
8535862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8545862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // We'll go through each time zone to find one with the same DST transitions and
8555862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // savings length
8565862a85e17e81866ca82a9905577931947fbd44eMarc Blank                for (String zoneId: zoneIds) {
8575862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // Get the TimeZone using the zoneId
8585862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    timeZone = TimeZone.getTimeZone(zoneId);
8595862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8605862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // Our strategy here is to check just before and just after the transitions
8615862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // and see whether the check for daylight time matches the expectation
8625862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // If both transitions match, then we have a match for the offset and start/end
8635862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // of dst.  That's the best we can do for now, since there's no other info
8645862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // provided by EAS (i.e. we can't get dynamic transitions, etc.)
8655862a85e17e81866ca82a9905577931947fbd44eMarc Blank
86679268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // Check one minute before and after DST start transition
86779268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    long millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstStart);
8682c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    Date before = new Date(millisAtTransition - precision);
8692c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    Date after = new Date(millisAtTransition + precision);
8705862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (timeZone.inDaylightTime(before)) continue;
8715862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (!timeZone.inDaylightTime(after)) continue;
8725862a85e17e81866ca82a9905577931947fbd44eMarc Blank
87379268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // Check one minute before and after DST end transition
87479268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstEnd);
87579268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // Note that we need to subtract an extra hour here, because we end up with
87679268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // gaining an hour in the transition BACK to standard time
8772c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    before = new Date(millisAtTransition - (dstSavings + precision));
8782c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    after = new Date(millisAtTransition + precision);
8795862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (!timeZone.inDaylightTime(before)) continue;
8805862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (timeZone.inDaylightTime(after)) continue;
8815862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8825862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // Check that the savings are the same
8835862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (dstSavings != timeZone.getDSTSavings()) continue;
88479268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    return timeZone;
8855862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
8862c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                boolean lenient = false;
887bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                boolean name = false;
8882c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                if ((dstStart.hour != dstEnd.hour) && (precision == STANDARD_DST_PRECISION)) {
8892c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    timeZone = tziStringToTimeZoneImpl(timeZoneString, LENIENT_DST_PRECISION);
8902c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    lenient = true;
8912c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                } else {
892bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                    // We can't find a time zone match, so our last attempt is to see if there's
893bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                    // a valid time zone name in the TZI; if not we'll just take the first TZ with
894bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                    // a matching offset (which is likely wrong, but ... what else is there to do)
895bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                    String tzName = getString(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_NAME_OFFSET,
896bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                            MSFT_TIME_ZONE_STRING_SIZE);
897bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                    if (!tzName.isEmpty()) {
898bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                        TimeZone tz = TimeZone.getTimeZone(tzName);
899bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                        if (tz != null) {
900bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                            timeZone = tz;
901bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                            name = true;
902bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                        } else {
903bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                            timeZone = TimeZone.getTimeZone(zoneIds[0]);
904bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                        }
905bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                    } else {
906bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                        timeZone = TimeZone.getTimeZone(zoneIds[0]);
907bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                    }
9082c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                }
909270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                if (Eas.USER_LOG) {
910bb0141b49e7eff978fa445249dc888461ea581e3Martin Hibdon                    LogUtils.d(TAG,
9112c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                            "No TimeZone with correct DST settings; using " +
912bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                            (name ? "name" : (lenient ? "lenient" : "first")) + ": " +
913bcc7188e6244176cd8b3915af50e5c0034307ba4Marc Blank                                    timeZone.getID());
914270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                }
915270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                return timeZone;
9165862a85e17e81866ca82a9905577931947fbd44eMarc Blank            }
9175862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
91879268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        return null;
9195862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
9205862a85e17e81866ca82a9905577931947fbd44eMarc Blank
9215c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank    static public String convertEmailDateTimeToCalendarDateTime(String date) {
9225c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        // Format for email date strings is 2010-02-23T16:00:00.000Z
92379268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        // Format for calendar date strings is 20100223T160000Z
9245c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank       return date.substring(0, 4) + date.substring(5, 7) + date.substring(8, 13) +
9255c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank           date.substring(14, 16) + date.substring(17, 19) + 'Z';
9265c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank    }
9275c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
928377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static String formatTwo(int num) {
929377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (num <= 12) {
930377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            return sTwoCharacterNumbers[num];
931377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        } else
932377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            return Integer.toString(num);
933377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
93414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
935377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
936377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Generate an EAS formatted date/time string based on GMT. See below for details.
937377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
938377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static public String millisToEasDateTime(long millis) {
939c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank        return millisToEasDateTime(millis, sGmtTimeZone, true);
940377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
9415862a85e17e81866ca82a9905577931947fbd44eMarc Blank
942377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
943e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank     * Generate a birthday string from a GregorianCalendar set appropriately; the format of this
944e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank     * string is YYYY-MM-DD
945e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank     * @param cal the calendar
946e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank     * @return the birthday string
947e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank     */
948e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank    static public String calendarToBirthdayString(GregorianCalendar cal) {
949e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        StringBuilder sb = new StringBuilder();
950e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        sb.append(cal.get(Calendar.YEAR));
951e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        sb.append('-');
952e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
953e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        sb.append('-');
954e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
955e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        return sb.toString();
956e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank    }
957e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank
958e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank    /**
959c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * Generate an EAS formatted local date/time string from a time and a time zone. If the final
960c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * argument is false, only a date will be returned (e.g. 20100331)
961377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param millis a time in milliseconds
962377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param tz a time zone
963c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * @param withTime if the time is to be included in the string
964c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * @return an EAS formatted string indicating the date (and time) in the given time zone
965377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
966c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank    static public String millisToEasDateTime(long millis, TimeZone tz, boolean withTime) {
967377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        StringBuilder sb = new StringBuilder();
968377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        GregorianCalendar cal = new GregorianCalendar(tz);
969377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        cal.setTimeInMillis(millis);
970377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sb.append(cal.get(Calendar.YEAR));
971377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
972377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
973c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank        if (withTime) {
974c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append('T');
975c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY)));
976c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append(formatTwo(cal.get(Calendar.MINUTE)));
977c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append(formatTwo(cal.get(Calendar.SECOND)));
978c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            if (tz == sGmtTimeZone) {
979c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                sb.append('Z');
980c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            }
981820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
982820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return sb.toString();
983820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
984820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
985820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
98610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Return the true minute at which a transition occurs
98710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Our transition time should be the in the minute BEFORE the transition
98810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * If this minute is 59, set minute to 0 and increment the hour
98910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * NOTE: We don't want to add a minute and retrieve minute/hour from the Calendar, because
99010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Calendar time will itself be influenced by the transition!  So adding 1 minute to
99110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * 01:59 (assume PST->PDT) will become 03:00, which isn't what we want (we want 02:00)
99210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     *
99310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @param calendar the calendar holding the transition date/time
99410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @return the true minute of the transition
99510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     */
99679268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static int getTrueTransitionMinute(GregorianCalendar calendar) {
99710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        int minute = calendar.get(Calendar.MINUTE);
99810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        if (minute == 59) {
99910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            minute = 0;
100010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        }
100110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        return minute;
100210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    }
100310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank
100410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    /**
100510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Return the true hour at which a transition occurs
100610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * See description for getTrueTransitionMinute, above
100710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @param calendar the calendar holding the transition date/time
100810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @return the true hour of the transition
100910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     */
101079268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static int getTrueTransitionHour(GregorianCalendar calendar) {
101110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        int hour = calendar.get(Calendar.HOUR_OF_DAY);
101210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        hour++;
101310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        if (hour == 24) {
101410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            hour = 0;
101510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        }
101610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        return hour;
101710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    }
101810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank
101910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    /**
102010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Generate a date/time string suitable for VTIMEZONE from a transition time in millis
102110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * The format is YYYYMMDDTHHMMSS
102210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @param millis a transition time in milliseconds
1023820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz a time zone
1024820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param dst whether we're entering daylight time
1025820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
102679268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static String transitionMillisToVCalendarTime(long millis, TimeZone tz, boolean dst) {
1027820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        StringBuilder sb = new StringBuilder();
1028820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar cal = new GregorianCalendar(tz);
1029820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        cal.setTimeInMillis(millis);
1030820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(cal.get(Calendar.YEAR));
1031820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
1032820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
1033820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append('T');
103410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        sb.append(formatTwo(getTrueTransitionHour(cal)));
103510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        sb.append(formatTwo(getTrueTransitionMinute(cal)));
103610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        sb.append(formatTwo(0));
1037377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return sb.toString();
1038377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
10395862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1040cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank    /**
1041270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * Returns a UTC calendar with year/month/day from local calendar and h/m/s/ms = 0
1042270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @param time the time in seconds of an all-day event in local time
1043270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @return the time in seconds in UTC
1044cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank     */
1045270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    static public long getUtcAllDayCalendarTime(long time, TimeZone localTimeZone) {
1046270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        return transposeAllDayTime(time, localTimeZone, UTC_TIMEZONE);
1047270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    }
1048270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank
1049270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    /**
1050270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * Returns a local calendar with year/month/day from UTC calendar and h/m/s/ms = 0
1051270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @param time the time in seconds of an all-day event in UTC
1052270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @return the time in seconds in local time
1053270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     */
1054270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    static public long getLocalAllDayCalendarTime(long time, TimeZone localTimeZone) {
1055270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        return transposeAllDayTime(time, UTC_TIMEZONE, localTimeZone);
1056270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    }
1057270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank
1058270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    static private long transposeAllDayTime(long time, TimeZone fromTimeZone,
1059270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank            TimeZone toTimeZone) {
1060270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        GregorianCalendar fromCalendar = new GregorianCalendar(fromTimeZone);
1061270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        fromCalendar.setTimeInMillis(time);
1062270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        GregorianCalendar toCalendar = new GregorianCalendar(toTimeZone);
1063cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank        // Set this calendar with correct year, month, and day, but zero hour, minute, and seconds
1064270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        toCalendar.set(fromCalendar.get(GregorianCalendar.YEAR),
1065270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                fromCalendar.get(GregorianCalendar.MONTH),
1066270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                fromCalendar.get(GregorianCalendar.DATE), 0, 0, 0);
10673baaee079e644467cf18c9b250ac30485f9c54e0Marc Blank        toCalendar.set(GregorianCalendar.MILLISECOND, 0);
1068270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        return toCalendar.getTimeInMillis();
1069cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank    }
1070cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank
1071377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static void addByDay(StringBuilder rrule, int dow, int wom) {
10725862a85e17e81866ca82a9905577931947fbd44eMarc Blank        rrule.append(";BYDAY=");
10735862a85e17e81866ca82a9905577931947fbd44eMarc Blank        boolean addComma = false;
10745862a85e17e81866ca82a9905577931947fbd44eMarc Blank        for (int i = 0; i < 7; i++) {
10755862a85e17e81866ca82a9905577931947fbd44eMarc Blank            if ((dow & 1) == 1) {
10765862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (addComma) {
10775862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    rrule.append(',');
10785862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
10795862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (wom > 0) {
10805862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // 5 = last week -> -1
10815862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // So -1SU = last sunday
10825862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    rrule.append(wom == 5 ? -1 : wom);
10835862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
10845862a85e17e81866ca82a9905577931947fbd44eMarc Blank                rrule.append(sDayTokens[i]);
10855862a85e17e81866ca82a9905577931947fbd44eMarc Blank                addComma = true;
10865862a85e17e81866ca82a9905577931947fbd44eMarc Blank            }
10875862a85e17e81866ca82a9905577931947fbd44eMarc Blank            dow >>= 1;
10885862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
10895862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
10905862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1091f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    static void addBySetpos(StringBuilder rrule, int dow, int wom) {
1092f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        // Indicate the days, but don't use wom in this case (it's used in the BYSETPOS);
1093f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        addByDay(rrule, dow, 0);
1094f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        rrule.append(";BYSETPOS=");
1095f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        rrule.append(wom == 5 ? "-1" : wom);
1096f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    }
1097f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank
10985862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static void addByMonthDay(StringBuilder rrule, int dom) {
10995862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // 127 means last day of the month
11005862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (dom == 127) {
11015862a85e17e81866ca82a9905577931947fbd44eMarc Blank            dom = -1;
11025862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
11035862a85e17e81866ca82a9905577931947fbd44eMarc Blank        rrule.append(";BYMONTHDAY=" + dom);
11045862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
11055862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1106f35b67cef20189c12a1a387dedf200eb30089725Marc Blank    /**
1107f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     * Generate the String version of the EAS integer for a given BYDAY value in an rrule
1108f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     * @param dow the BYDAY value of the rrule
1109f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     * @return the String version of the EAS value of these days
1110f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     */
111114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    static String generateEasDayOfWeek(String dow) {
1112f35b67cef20189c12a1a387dedf200eb30089725Marc Blank        int bits = 0;
111314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int bit = 1;
111414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        for (String token: sDayTokens) {
1115f35b67cef20189c12a1a387dedf200eb30089725Marc Blank            // If we can find the day in the dow String, add the bit to our bits value
1116f35b67cef20189c12a1a387dedf200eb30089725Marc Blank            if (dow.indexOf(token) >= 0) {
1117f35b67cef20189c12a1a387dedf200eb30089725Marc Blank                bits |= bit;
111814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            }
1119f35b67cef20189c12a1a387dedf200eb30089725Marc Blank            bit <<= 1;
112014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        }
1121f35b67cef20189c12a1a387dedf200eb30089725Marc Blank        return Integer.toString(bits);
112214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    }
112314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
1124377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
1125377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Extract the value of a token in an RRULE string
1126377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param rrule an RRULE string
1127377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param token a token to look for in the RRULE
1128377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return the value of that token
1129377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
113014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    static String tokenFromRrule(String rrule, String token) {
113114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int start = rrule.indexOf(token);
113214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        if (start < 0) return null;
113314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int len = rrule.length();
113414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        start += token.length();
113514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int end = start;
113614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        char c;
113714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        do {
113814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            c = rrule.charAt(end++);
1139b129a5f1b340ae6362397685c407099ceae8d9e9Marc Blank            if ((c == ';') || (end == len)) {
114014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                if (end == len) end++;
114114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                return rrule.substring(start, end -1);
114214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            }
1143377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        } while (true);
114414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    }
114514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
114614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    /**
114721c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank     * Reformat an RRULE style UNTIL to an EAS style until
114821c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank     */
114960df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank    @VisibleForTesting
11502f369a47e14916a34f49c79c0a246a2e3ac3072fJay Shrauner    static String recurrenceUntilToEasUntil(String until) throws ParseException {
115160df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // Get a calendar in our local time zone
115260df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        GregorianCalendar localCalendar = new GregorianCalendar(TimeZone.getDefault());
115360df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // Set the time per GMT time in the 'until'
115460df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        localCalendar.setTimeInMillis(Utility.parseDateTimeToMillis(until));
115521c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        StringBuilder sb = new StringBuilder();
115660df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // Build a string with local year/month/date
115760df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        sb.append(localCalendar.get(Calendar.YEAR));
115860df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        sb.append(formatTwo(localCalendar.get(Calendar.MONTH) + 1));
115960df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        sb.append(formatTwo(localCalendar.get(Calendar.DAY_OF_MONTH)));
116060df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // EAS ignores the time in 'until'; go figure
1161e3e9ef55a2afd2c121e1f214a8fa34fced3bac38Marc Blank        sb.append("T000000Z");
116221c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        return sb.toString();
116321c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    }
116421c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank
116521c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    /**
1166f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank     * Convenience method to add "count", "interval", and "until" to an EAS calendar stream
1167f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank     * According to EAS docs, OCCURRENCES must always come before INTERVAL
116821c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank     */
1169f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank    static private void addCountIntervalAndUntil(String rrule, Serializer s) throws IOException {
1170f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        String count = tokenFromRrule(rrule, "COUNT=");
1171f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (count != null) {
1172f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            s.data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, count);
1173f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
1174f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        String interval = tokenFromRrule(rrule, "INTERVAL=");
1175f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (interval != null) {
1176f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, interval);
1177f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
117821c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        String until = tokenFromRrule(rrule, "UNTIL=");
117921c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        if (until != null) {
11802f369a47e14916a34f49c79c0a246a2e3ac3072fJay Shrauner            try {
11812f369a47e14916a34f49c79c0a246a2e3ac3072fJay Shrauner                s.data(Tags.CALENDAR_RECURRENCE_UNTIL, recurrenceUntilToEasUntil(until));
11822f369a47e14916a34f49c79c0a246a2e3ac3072fJay Shrauner            } catch (ParseException e) {
11832f369a47e14916a34f49c79c0a246a2e3ac3072fJay Shrauner                LogUtils.w(TAG, "Parse error for CALENDAR_RECURRENCE_UNTIL tag.", e);
11842f369a47e14916a34f49c79c0a246a2e3ac3072fJay Shrauner            }
118521c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        }
118621c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    }
118721c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank
1188f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank    static private void addByDay(String byDay, Serializer s) throws IOException {
1189f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        // This can be 1WE (1st Wednesday) or -1FR (last Friday)
1190f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        int weekOfMonth = byDay.charAt(0);
1191f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        String bareByDay;
1192f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (weekOfMonth == '-') {
1193f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            // -1 is the only legal case (last week) Use "5" for EAS
1194f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            weekOfMonth = 5;
1195f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            bareByDay = byDay.substring(2);
1196f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        } else {
1197f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            weekOfMonth = weekOfMonth - '0';
1198f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            bareByDay = byDay.substring(1);
1199f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
1200f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
1201f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
1202f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank    }
1203f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank
1204f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    static private void addByDaySetpos(String byDay, String bySetpos, Serializer s)
1205f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            throws IOException {
1206f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        int weekOfMonth = bySetpos.charAt(0);
1207f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        if (weekOfMonth == '-') {
1208f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            // -1 is the only legal case (last week) Use "5" for EAS
1209f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            weekOfMonth = 5;
1210f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        } else {
1211f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            weekOfMonth = weekOfMonth - '0';
1212f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        }
1213f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
1214f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
1215f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    }
1216f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank
121721c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    /**
121814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * Write recurrence information to EAS based on the RRULE in CalendarProvider
1219d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert     *
122014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @param rrule the RRULE, from CalendarProvider
122114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @param startTime, the DTSTART of this Event
1222d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert     * @param timeZone the time zone of the Event
122314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @param s the Serializer we're using to write WBXML data
1224d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert     *
122514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @throws IOException
122614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     */
122714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    // NOTE: For the moment, we're only parsing recurrence types that are supported by the
1228c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    // Calendar app UI, which is a subset of possible recurrence types
122914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    // This code must be updated when the Calendar adds new functionality
1230d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert    static public void recurrenceFromRrule(String rrule, long startTime, TimeZone timeZone,
1231d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert            Serializer s)
123279268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank            throws IOException {
123388683d3ebf9720ee648220dd6884e200d5e87038Marc Blank        if (Eas.USER_LOG) {
1234bb0141b49e7eff978fa445249dc888461ea581e3Martin Hibdon            LogUtils.d(TAG, "RRULE: " + rrule);
123588683d3ebf9720ee648220dd6884e200d5e87038Marc Blank        }
1236d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert        final String freq = tokenFromRrule(rrule, "FREQ=");
123714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        // If there's no FREQ=X, then we don't write a recurrence
123814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        // Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the
123914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        // possibility of writing out a partial recurrence stanza
124014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        if (freq != null) {
124114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            if (freq.equals("DAILY")) {
124214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.start(Tags.CALENDAR_RECURRENCE);
124314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0");
1244f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                addCountIntervalAndUntil(rrule, s);
124514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.end();
124614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            } else if (freq.equals("WEEKLY")) {
124714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.start(Tags.CALENDAR_RECURRENCE);
124814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1");
124914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                // Requires a day of week (whereas RRULE does not)
1250f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                addCountIntervalAndUntil(rrule, s);
1251d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                final String byDay = tokenFromRrule(rrule, "BYDAY=");
125214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                if (byDay != null) {
125314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
1254f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    // Find week number (1-4 and 5 for last)
1255f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    if (byDay.startsWith("-1")) {
1256f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, "5");
1257f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    } else {
1258d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        final char c = byDay.charAt(0);
1259f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        if (c >= '1' && c <= '4') {
1260f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                            s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, byDay.substring(0, 1));
1261f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        }
1262f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    }
126314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                }
126414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.end();
126514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            } else if (freq.equals("MONTHLY")) {
126614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
126714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                if (byMonthDay != null) {
126814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    s.start(Tags.CALENDAR_RECURRENCE);
1269f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    // Special case for last day of month
1270d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                    if (byMonthDay.equals("-1")) {
1271f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
1272f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        addCountIntervalAndUntil(rrule, s);
1273f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "127");
1274f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    } else {
1275f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        // The nth day of the month
1276f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
1277f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        addCountIntervalAndUntil(rrule, s);
1278f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
1279f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    }
128014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    s.end();
128114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                } else {
1282d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                    final String byDay = tokenFromRrule(rrule, "BYDAY=");
1283d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                    final String bySetpos = tokenFromRrule(rrule, "BYSETPOS=");
128414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    if (byDay != null) {
128514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                        s.start(Tags.CALENDAR_RECURRENCE);
128614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
1287f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                        addCountIntervalAndUntil(rrule, s);
1288f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        if (bySetpos != null) {
1289f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                            addByDaySetpos(byDay, bySetpos, s);
1290f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        } else {
1291f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                            addByDay(byDay, s);
1292f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        }
129314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                        s.end();
1294d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                    } else {
1295d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        // Neither BYDAY or BYMONTHDAY implies it's BYMONTHDAY based on DTSTART
1296d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        // Calculate the day from the startDate
1297d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        s.start(Tags.CALENDAR_RECURRENCE);
1298d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        final GregorianCalendar cal = new GregorianCalendar();
1299d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        cal.setTimeInMillis(startTime);
1300d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        cal.setTimeZone(timeZone);
1301d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
1302d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
1303d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        addCountIntervalAndUntil(rrule, s);
1304d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
1305d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                        s.end();
130614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    }
130714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                }
130814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            } else if (freq.equals("YEARLY")) {
130914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byMonth = tokenFromRrule(rrule, "BYMONTH=");
131014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
1311d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                final String byDay = tokenFromRrule(rrule, "BYDAY=");
1312f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                if (byMonth == null && byMonthDay == null) {
131314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    // Calculate the month and day from the startDate
1314d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                    final GregorianCalendar cal = new GregorianCalendar();
131514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    cal.setTimeInMillis(startTime);
1316d9cfcc6c265974e9bcff93fc4541b402afdfa116Alon Albert                    cal.setTimeZone(timeZone);
131714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1);
131814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
131914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                }
1320f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                if (byMonth != null && (byMonthDay != null || byDay != null)) {
1321f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.start(Tags.CALENDAR_RECURRENCE);
1322f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.data(Tags.CALENDAR_RECURRENCE_TYPE, byDay == null ? "5" : "6");
1323f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    addCountIntervalAndUntil(rrule, s);
1324f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
1325f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    // Note that both byMonthDay and byDay can't be true in a valid RRULE
1326f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    if (byMonthDay != null) {
1327f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
1328f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    } else {
1329f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                        addByDay(byDay, s);
1330f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    }
1331f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.end();
1332f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                }
1333377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
133414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        }
133514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    }
133614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
1337377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
1338377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Build an RRULE String from EAS recurrence information
1339377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param type the type of recurrence
1340377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param occurrences how many recurrences (instances)
1341377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param interval the interval between recurrences
1342377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param dow day of the week
1343377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param dom day of the month
1344377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param wom week of the month
1345377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param moy month of the year
1346377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param until the last recurrence time
1347377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return a valid RRULE String
1348377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
13495862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow,
13505862a85e17e81866ca82a9905577931947fbd44eMarc Blank            int dom, int wom, int moy, String until) {
13510c49fe537fed155ecf0df57065255a738f69e193Jay Shrauner        if (type < 0 || type >= sTypeToFreq.length) {
13520c49fe537fed155ecf0df57065255a738f69e193Jay Shrauner            return null;
13530c49fe537fed155ecf0df57065255a738f69e193Jay Shrauner        }
13540c49fe537fed155ecf0df57065255a738f69e193Jay Shrauner        final String typeStr = sTypeToFreq[type];
13550c49fe537fed155ecf0df57065255a738f69e193Jay Shrauner        // Type array is sparse (eg, no type 4), so catch invalid (empty) types
13560c49fe537fed155ecf0df57065255a738f69e193Jay Shrauner        if (TextUtils.isEmpty(typeStr)) {
13570c49fe537fed155ecf0df57065255a738f69e193Jay Shrauner            return null;
13580c49fe537fed155ecf0df57065255a738f69e193Jay Shrauner        }
13590c49fe537fed155ecf0df57065255a738f69e193Jay Shrauner        StringBuilder rrule = new StringBuilder("FREQ=" + typeStr);
13605862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // INTERVAL and COUNT
13615862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (occurrences > 0) {
13625862a85e17e81866ca82a9905577931947fbd44eMarc Blank            rrule.append(";COUNT=" + occurrences);
13635862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
1364f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (interval > 0) {
1365f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            rrule.append(";INTERVAL=" + interval);
1366f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
13675862a85e17e81866ca82a9905577931947fbd44eMarc Blank
13685862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Days, weeks, months, etc.
13695862a85e17e81866ca82a9905577931947fbd44eMarc Blank        switch(type) {
13705862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 0: // DAILY
13715862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 1: // WEEKLY
1372f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                if (dow > 0) addByDay(rrule, dow, wom);
13735862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
13745862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 2: // MONTHLY
13755862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (dom > 0) addByMonthDay(rrule, dom);
13765862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
13775862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 3: // MONTHLY (on the nth day)
1378f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                // 127 is a special case meaning "last day of the month"
1379f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                if (dow == 127) {
1380f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    rrule.append(";BYMONTHDAY=-1");
1381f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                // week 5 and dow = weekdays -> last weekday (need BYSETPOS)
1382e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank                } else if ((wom == 5 || wom == 1) && (dow == EAS_WEEKDAYS || dow == EAS_WEEKENDS)) {
1383f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    addBySetpos(rrule, dow, wom);
1384f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                } else if (dow > 0) addByDay(rrule, dow, wom);
13855862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
1386820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            case 5: // YEARLY (specific day)
13875862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (dom > 0) addByMonthDay(rrule, dom);
13885862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (moy > 0) {
13895862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    rrule.append(";BYMONTH=" + moy);
13905862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
13915862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
1392820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            case 6: // YEARLY
13935862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (dow > 0) addByDay(rrule, dow, wom);
1394820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (dom > 0) addByMonthDay(rrule, dom);
1395820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (moy > 0) {
1396820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    rrule.append(";BYMONTH=" + moy);
1397820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
13985862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
13995862a85e17e81866ca82a9905577931947fbd44eMarc Blank            default:
14005862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
14015862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
14025862a85e17e81866ca82a9905577931947fbd44eMarc Blank
14035862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // UNTIL comes last
14045862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (until != null) {
140521c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank            rrule.append(";UNTIL=" + until);
14065862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
14075862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1408f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        if (Eas.USER_LOG) {
1409942b7d73f2f5b3d6c651e39463e615fe6902a910Scott Kennedy            LogUtils.d(Logging.LOG_TAG, "Created rrule: " + rrule);
1410f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        }
14115862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return rrule.toString();
14125862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
141377110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank
141477110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank    /**
141577110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * Create a Calendar in CalendarProvider to which synced Events will be linked
14166e66ab513197793c34f5dcda159043da39224ff9Yu Ping Hu     * @param context
14176e66ab513197793c34f5dcda159043da39224ff9Yu Ping Hu     * @param contentResolver
141877110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @param account the account being synced
141977110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @param mailbox the Exchange mailbox for the calendar
142077110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @return the unique id of the Calendar
142177110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     */
14226e66ab513197793c34f5dcda159043da39224ff9Yu Ping Hu    static public long createCalendar(final Context context, final ContentResolver contentResolver,
14236e66ab513197793c34f5dcda159043da39224ff9Yu Ping Hu            final Account account, final Mailbox mailbox) {
142477110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // Create a Calendar object
142577110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        ContentValues cv = new ContentValues();
142677110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // TODO How will this change if the user changes his account display name?
14271424b228e105b1c5c8b54eb0f401a549161f8f5fAlon Albert        cv.put(Calendars.CALENDAR_DISPLAY_NAME, mailbox.mDisplayName);
14289e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        cv.put(Calendars.ACCOUNT_NAME, account.mEmailAddress);
14299e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        cv.put(Calendars.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
143077110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        cv.put(Calendars.SYNC_EVENTS, 1);
14311424b228e105b1c5c8b54eb0f401a549161f8f5fAlon Albert        cv.put(Calendars._SYNC_ID, mailbox.mServerId);
1432443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.VISIBLE, 1);
14335a02f79c91df6df44f3c95742f61f2c25c464cc3Marc Blank        // Don't show attendee status if we're the organizer
1434443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.CAN_ORGANIZER_RESPOND, 0);
1435443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.CAN_MODIFY_TIME_ZONE, 0);
1436443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.MAX_REMINDERS, 1);
143780a8e57ce2dc2695ed6f35599d326090e4ad9faeRoboErik        cv.put(Calendars.ALLOWED_REMINDERS, ALLOWED_REMINDER_TYPES);
1438937af5abcbc1268f22a3058b00835c74ba20f116RoboErik        cv.put(Calendars.ALLOWED_ATTENDEE_TYPES, ALLOWED_ATTENDEE_TYPES);
1439937af5abcbc1268f22a3058b00835c74ba20f116RoboErik        cv.put(Calendars.ALLOWED_AVAILABILITY, ALLOWED_AVAILABILITIES);
14405a02f79c91df6df44f3c95742f61f2c25c464cc3Marc Blank
144177110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // TODO Coordinate account colors w/ Calendar, if possible
14426e66ab513197793c34f5dcda159043da39224ff9Yu Ping Hu        int color = new AccountServiceProxy(context).getAccountColor(account.mId);
14439e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        cv.put(Calendars.CALENDAR_COLOR, color);
144404c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik        cv.put(Calendars.CALENDAR_TIME_ZONE, Time.getCurrentTimezone());
144504c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik        cv.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
144677110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        cv.put(Calendars.OWNER_ACCOUNT, account.mEmailAddress);
144777110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank
14486e66ab513197793c34f5dcda159043da39224ff9Yu Ping Hu        Uri uri = contentResolver.insert(asSyncAdapter(Calendars.CONTENT_URI, account.mEmailAddress,
14496989716b639d274a98141674556ac9402be32ebeRoboErik                        Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
145077110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // We save the id of the calendar into mSyncStatus
145177110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        if (uri != null) {
145277110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank            String stringId = uri.getPathSegments().get(1);
145377110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank            mailbox.mSyncStatus = stringId;
145477110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank            return Long.parseLong(stringId);
145577110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        }
145677110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        return -1;
145777110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank    }
1458c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
14596989716b639d274a98141674556ac9402be32ebeRoboErik    static Uri asSyncAdapter(Uri uri, String account, String accountType) {
14606989716b639d274a98141674556ac9402be32ebeRoboErik        return uri.buildUpon()
1461693ed7fdd5a7ec7af87d105b76267c78a8acc3dbRoboErik                .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,
1462693ed7fdd5a7ec7af87d105b76267c78a8acc3dbRoboErik                        "true")
14636989716b639d274a98141674556ac9402be32ebeRoboErik                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
14646989716b639d274a98141674556ac9402be32ebeRoboErik                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
14656989716b639d274a98141674556ac9402be32ebeRoboErik    }
14666989716b639d274a98141674556ac9402be32ebeRoboErik
1467b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank    /**
1468b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     * Return the uid for an event based on its globalObjId
1469b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     * @param globalObjId the base64 encoded String provided by EAS
1470b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     * @return the uid for the calendar event
1471b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     */
1472b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank    static public String getUidFromGlobalObjId(String globalObjId) {
1473b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        StringBuilder sb = new StringBuilder();
1474b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        // First get the decoded base64
1475b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        try {
1476b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            byte[] idBytes = Base64.decode(globalObjId, Base64.DEFAULT);
1477b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            String idString = new String(idBytes);
1478b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // If the base64 decoded string contains the magic substring: "vCal-Uid", then
1479b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // the actual uid is hidden within; the magic substring is never at the start of the
1480b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // decoded base64
1481b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            int index = idString.indexOf("vCal-Uid");
1482b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            if (index > 0) {
1483b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // The uid starts after "vCal-Uidxxxx", where xxxx are padding
1484b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // characters.  And it ends before the last character, which is ascii 0
1485b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                return idString.substring(index + 12, idString.length() - 1);
1486b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            } else {
1487b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // This is an EAS uid. Go through the bytes and write out the hex
1488b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // values as characters; this is what we'll need to pass back to EAS
1489b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // when responding to the invitation
1490b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                for (byte b: idBytes) {
1491b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                    Utility.byteToHex(sb, b);
1492b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                }
1493b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                return sb.toString();
1494b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            }
1495b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        } catch (RuntimeException e) {
1496b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // In the worst of cases (bad format, etc.), we can always return the input
1497b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            return globalObjId;
1498b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        }
1499b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank    }
1500b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank
1501dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    /**
1502dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * Get a selfAttendeeStatus from a busy status
1503dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * The default here is NONE (i.e. we don't know the status)
1504dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * Note that a busy status of FREE must mean NONE as well, since it can't mean declined
1505dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * (there would be no event)
1506dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @param busyStatus the busy status, from EAS
1507dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @return the corresponding value for selfAttendeeStatus
1508dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     */
1509edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank    static public int attendeeStatusFromBusyStatus(int busyStatus) {
1510edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank        int attendeeStatus;
1511dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        switch (busyStatus) {
1512dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case BUSY_STATUS_BUSY:
1513edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
1514dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1515dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case BUSY_STATUS_TENTATIVE:
1516edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
1517dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1518dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case BUSY_STATUS_FREE:
1519e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank            case BUSY_STATUS_OUT_OF_OFFICE:
1520dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            default:
1521edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
1522dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        }
1523edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank        return attendeeStatus;
1524dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    }
1525dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank
152665d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    /**
1527f9d3d43800dcb522a7c492e96d490eca9f120e43Marc Blank     * Get a selfAttendeeStatus from a response type (EAS 14+)
1528f9d3d43800dcb522a7c492e96d490eca9f120e43Marc Blank     * The default here is NONE (i.e. we don't know the status), though in theory this can't happen
1529f9d3d43800dcb522a7c492e96d490eca9f120e43Marc Blank     * @param busyStatus the response status, from EAS
153065d022dc43e4461e86fd7bc143591f542b07428bMarc Blank     * @return the corresponding value for selfAttendeeStatus
153165d022dc43e4461e86fd7bc143591f542b07428bMarc Blank     */
153265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    static public int attendeeStatusFromResponseType(int responseType) {
153365d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        int attendeeStatus;
153465d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        switch (responseType) {
153565d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_NOT_RESPONDED:
15361bbcf25030104af1631ae74b3e1c3033e09ea4edAlon Albert                attendeeStatus = Attendees.ATTENDEE_STATUS_INVITED;
153765d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
153865d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_ACCEPTED:
153965d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
154065d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
154165d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_TENTATIVE:
154265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
154365d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
154465d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_DECLINED:
154565d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_DECLINED;
154665d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
154765d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            default:
154865d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
154965d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        }
155065d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        return attendeeStatus;
155165d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    }
155265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank
1553dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    /** Get a busy status from a selfAttendeeStatus
1554dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * The default here is BUSY
1555dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @param selfAttendeeStatus from CalendarProvider2
1556dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @return the corresponding value of busy status
1557dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     */
1558edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank    static public int busyStatusFromAttendeeStatus(int selfAttendeeStatus) {
1559dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        int busyStatus;
1560dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        switch (selfAttendeeStatus) {
1561dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_DECLINED:
1562dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_NONE:
1563dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_INVITED:
1564dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                busyStatus = BUSY_STATUS_FREE;
1565dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1566dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_TENTATIVE:
1567dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                busyStatus = BUSY_STATUS_TENTATIVE;
1568dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1569dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_ACCEPTED:
1570dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            default:
1571dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                busyStatus = BUSY_STATUS_BUSY;
1572dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1573dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        }
1574dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        return busyStatus;
1575dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    }
1576dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank
1577e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    /** Get a busy status from event availability
1578e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * The default here is TENTATIVE
1579e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * @param availability from CalendarProvider2
1580e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * @return the corresponding value of busy status
1581e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     */
1582e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    static public int busyStatusFromAvailability(int availability) {
1583e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        int busyStatus;
1584e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        switch (availability) {
1585e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case Events.AVAILABILITY_BUSY:
1586e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                busyStatus = BUSY_STATUS_BUSY;
1587e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1588e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case Events.AVAILABILITY_FREE:
1589e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                busyStatus = BUSY_STATUS_FREE;
1590e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1591e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case Events.AVAILABILITY_TENTATIVE:
1592e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            default:
1593e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                busyStatus = BUSY_STATUS_TENTATIVE;
1594e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1595e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        }
1596e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        return busyStatus;
1597e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    }
1598e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank
1599e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    /** Get an event availability from busy status
1600e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * The default here is TENTATIVE
1601e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * @param busyStatus from CalendarProvider2
1602e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * @return the corresponding availability value
1603e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     */
1604e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    static public int availabilityFromBusyStatus(int busyStatus) {
1605e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        int availability;
1606e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        switch (busyStatus) {
1607e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case BUSY_STATUS_BUSY:
1608e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                availability = Events.AVAILABILITY_BUSY;
1609e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1610e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case BUSY_STATUS_FREE:
1611e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                availability = Events.AVAILABILITY_FREE;
1612e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1613e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case BUSY_STATUS_TENTATIVE:
1614e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            default:
1615e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                availability = Events.AVAILABILITY_TENTATIVE;
1616e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1617e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        }
1618e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        return availability;
1619e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    }
1620e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank
1621dafc866120dac68fabbddcc9943e3995894c5244Marc Blank    static public String buildMessageTextFromEntityValues(Context context,
1622dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            ContentValues entityValues, StringBuilder sb) {
1623dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        if (sb == null) {
1624dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            sb = new StringBuilder();
1625dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        }
1626dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        Resources resources = context.getResources();
1627dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // TODO: Add more detail to message text
1628dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // Right now, we're using.. When: Tuesday, March 5th at 2:00pm
1629dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // What we're missing is the duration and any recurrence information.  So this should be
1630dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // more like... When: Tuesdays, starting March 5th from 2:00pm - 3:00pm
1631dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // This would require code to build complex strings, and it will have to wait
16329ca8918b82221dd441293973ffb84d565a52993aMarc Blank        // For now, we'll just use the meeting_recurring string
1633f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank
1634f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        boolean allDayEvent = false;
1635f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        if (entityValues.containsKey(Events.ALL_DAY)) {
1636f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
1637f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            allDayEvent = (ade != null) && (ade == 1);
1638f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        }
1639f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        boolean recurringEvent = !entityValues.containsKey(Events.ORIGINAL_SYNC_ID) &&
1640f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            entityValues.containsKey(Events.RRULE);
1641f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank
1642a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner        if (entityValues.containsKey(Events.DTSTART)) {
1643a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner            final String dateTimeString;
1644a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner            final int res;
1645a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner            final long startTime = entityValues.getAsLong(Events.DTSTART);
1646a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner            if (allDayEvent) {
1647a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner                final Date date = new Date(getLocalAllDayCalendarTime(startTime,
1648a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner                                TimeZone.getDefault()));
1649a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner                dateTimeString = DateFormat.getDateInstance().format(date);
1650a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner                res = recurringEvent ? R.string.meeting_allday_recurring
1651a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner                    : R.string.meeting_allday;
1652a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner            } else {
1653a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner                dateTimeString = DateFormat.getDateTimeInstance().format(
1654a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner                        new Date(startTime));
1655a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner                res = recurringEvent ? R.string.meeting_recurring
1656a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner                    : R.string.meeting_when;
1657a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner            }
1658a53b7d0cefc73c63ac83708dfc4b554bdf01b1b6Jay Shrauner            sb.append(resources.getString(res, dateTimeString));
16599ca8918b82221dd441293973ffb84d565a52993aMarc Blank        }
1660f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank
1661dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        String location = null;
1662dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        if (entityValues.containsKey(Events.EVENT_LOCATION)) {
1663dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            location = entityValues.getAsString(Events.EVENT_LOCATION);
1664efae936b117c9e4f3056d52fdbfe4d3f261483e5Marc Blank            if (!TextUtils.isEmpty(location)) {
1665dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                sb.append("\n");
1666dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                sb.append(resources.getString(R.string.meeting_where, location));
1667dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            }
1668dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        }
1669dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // If there's a description for this event, append it
1670dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        String desc = entityValues.getAsString(Events.DESCRIPTION);
1671dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        if (desc != null) {
1672dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            sb.append("\n--\n");
1673dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            sb.append(desc);
1674dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        }
1675dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        return sb.toString();
1676dafc866120dac68fabbddcc9943e3995894c5244Marc Blank    }
1677dafc866120dac68fabbddcc9943e3995894c5244Marc Blank
1678c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    /**
167924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * Add an attendee to the ics attachment and the to list of the Message being composed
168024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param ics the ics attachment writer
168124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param toList the list of addressees for this email
168224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param attendeeName the name of the attendee
168324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param attendeeEmail the email address of the attendee
168424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param messageFlag the flag indicating the action to be indicated by the message
168524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param account the sending account of the email
168624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     */
168724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank    static private void addAttendeeToMessage(SimpleIcsWriter ics, ArrayList<Address> toList,
168824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            String attendeeName, String attendeeEmail, int messageFlag, Account account) {
168924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank        if ((messageFlag & Message.FLAG_OUTGOING_MEETING_REQUEST_MASK) != 0) {
169024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            String icalTag = ICALENDAR_ATTENDEE_INVITE;
169124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if ((messageFlag & Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
169224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                icalTag = ICALENDAR_ATTENDEE_CANCEL;
169324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
169424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (attendeeName != null) {
169524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(attendeeName);
169624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
169724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
169824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            toList.add(attendeeName == null ? new Address(attendeeEmail) :
169924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                new Address(attendeeEmail, attendeeName));
170024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank        } else if (attendeeEmail.equalsIgnoreCase(account.mEmailAddress)) {
170124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            String icalTag = null;
170224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            switch (messageFlag) {
170324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                case Message.FLAG_OUTGOING_MEETING_ACCEPT:
170424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag = ICALENDAR_ATTENDEE_ACCEPT;
170524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    break;
170624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                case Message.FLAG_OUTGOING_MEETING_DECLINE:
170724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag = ICALENDAR_ATTENDEE_DECLINE;
170824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    break;
170924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
171024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag = ICALENDAR_ATTENDEE_TENTATIVE;
171124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    break;
171224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
171324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (icalTag != null) {
171424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                if (attendeeName != null) {
171524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag += ";CN="
171624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                            + SimpleIcsWriter.quoteParamValue(attendeeName);
171724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                }
171824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
171924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
172024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank        }
172124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank    }
172224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
172324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank    /**
1724c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * Create a Message for an (Event) Entity
1725c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param entity the Entity for the Event (as might be retrieved by CalendarProvider)
1726c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent
1727275e73703e09a1211ae6aa6fc2a43226e1fcdeedMartin Hibdon     * @param uid the unique id of this Event, or null if it can be retrieved from the Event
1728275e73703e09a1211ae6aa6fc2a43226e1fcdeedMartin Hibdon     * @param account the user's account
1729c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @return a Message with many fields pre-filled (more later)
1730c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     */
1731c8e4352ea6cfa67f15140512e84af8ccede222d2Marc Blank    static public Message createMessageForEntity(Context context, Entity entity,
17325c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            int messageFlag, String uid, Account account) {
1733601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank        return createMessageForEntity(context, entity, messageFlag, uid, account,
173424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                null /*specifiedAttendee*/);
1735601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    }
1736601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank
1737601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    static public EmailContent.Message createMessageForEntity(Context context, Entity entity,
173824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            int messageFlag, String uid, Account account, String specifiedAttendee) {
1739c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        ContentValues entityValues = entity.getEntityValues();
17405c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        ArrayList<NamedContentValues> subValues = entity.getSubValues();
174104e4cdadbe23670fd99d21ffb88af40ef77d69aeJay Shrauner        boolean isException = entityValues.containsKey(Events.ORIGINAL_INSTANCE_TIME);
1742f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank        boolean isReply = false;
1743c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1744c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        EmailContent.Message msg = new EmailContent.Message();
1745c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        msg.mFlags = messageFlag;
1746c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        msg.mTimeStamp = System.currentTimeMillis();
1747c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1748c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        String method;
1749f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank        if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE) != 0) {
1750c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            method = "REQUEST";
1751f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank        } else if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
1752f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            method = "CANCEL";
1753c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        } else {
1754c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            method = "REPLY";
1755f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            isReply = true;
1756c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        }
1757c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
17588d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank        try {
17595c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // Create our iCalendar writer and start generating tags
17605c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            SimpleIcsWriter ics = new SimpleIcsWriter();
17618d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("BEGIN", "VCALENDAR");
17628d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("METHOD", method);
17638d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("PRODID", "AndroidEmail");
17648d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("VERSION", "2.0");
1765820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
1766820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Our default vcalendar time zone is UTC, but this will change (below) if we're
1767820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // sending a recurring event, in which case we use local time
1768820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            TimeZone vCalendarTimeZone = sGmtTimeZone;
1769c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            String vCalendarDateSuffix = "";
1770c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank
1771c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            // Check for all day event
1772c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            boolean allDayEvent = false;
1773c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            if (entityValues.containsKey(Events.ALL_DAY)) {
1774c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
1775c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                allDayEvent = (ade != null) && (ade == 1);
1776c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                if (allDayEvent) {
1777c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                    // Example: DTSTART;VALUE=DATE:20100331 (all day event)
1778c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                    vCalendarDateSuffix = ";VALUE=DATE";
1779c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                }
1780c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            }
1781820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
1782820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // If we're inviting people and the meeting is recurring, we need to send our time zone
1783c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            // information and make sure to send DTSTART/DTEND in local time (unless, of course,
1784b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank            // this is an all-day event).  Recurring, for this purpose, includes exceptions to
1785b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank            // recurring events
1786b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank            if (!isReply && !allDayEvent &&
1787b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank                    (entityValues.containsKey(Events.RRULE) ||
17889e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik                            entityValues.containsKey(Events.ORIGINAL_SYNC_ID))) {
1789820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                vCalendarTimeZone = TimeZone.getDefault();
1790820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Write the VTIMEZONE block to the writer
1791820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                timeZoneToVTimezone(vCalendarTimeZone, ics);
1792c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                // Example: DTSTART;TZID=US/Pacific:20100331T124500
1793c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                vCalendarDateSuffix = ";TZID=" + vCalendarTimeZone.getID();
1794820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
1795820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
17968d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("BEGIN", "VEVENT");
17978d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (uid == null) {
179804c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik                uid = entityValues.getAsString(Events.SYNC_DATA2);
17998d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
18008d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (uid != null) {
18018d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                ics.writeTag("UID", uid);
18028d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
1803c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
18045c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            if (entityValues.containsKey("DTSTAMP")) {
18055c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                ics.writeTag("DTSTAMP", entityValues.getAsString("DTSTAMP"));
18065c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            } else {
180779268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                ics.writeTag("DTSTAMP", millisToEasDateTime(System.currentTimeMillis()));
18088d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
1809c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
18108d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            long startTime = entityValues.getAsLong(Events.DTSTART);
18115c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            if (startTime != 0) {
1812c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                ics.writeTag("DTSTART" + vCalendarDateSuffix,
1813c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                        millisToEasDateTime(startTime, vCalendarTimeZone, !allDayEvent));
18145c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            }
1815c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1816dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            // If this is an Exception, we send the recurrence-id, which is just the original
1817dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            // instance time
1818dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            if (isException) {
181904e4cdadbe23670fd99d21ffb88af40ef77d69aeJay Shrauner                // isException indicates this key is present
1820dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
1821c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                ics.writeTag("RECURRENCE-ID" + vCalendarDateSuffix,
1822c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                        millisToEasDateTime(originalTime, vCalendarTimeZone, !allDayEvent));
1823dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            }
1824dafc866120dac68fabbddcc9943e3995894c5244Marc Blank
18258d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (!entityValues.containsKey(Events.DURATION)) {
18268d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                if (entityValues.containsKey(Events.DTEND)) {
1827c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                    ics.writeTag("DTEND" + vCalendarDateSuffix,
182879268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                            millisToEasDateTime(
1829c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                                    entityValues.getAsLong(Events.DTEND), vCalendarTimeZone,
1830c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                                    !allDayEvent));
18318d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
18328d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            } else {
18338d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // Convert this into millis and add it to DTSTART for DTEND
18348d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // We'll use 1 hour as a default
18358d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                long durationMillis = HOURS;
18368d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                Duration duration = new Duration();
18378d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                try {
18388d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    duration.parse(entityValues.getAsString(Events.DURATION));
183989bee1e3d03b439f4084bc9962bb3cbffd0b878aMarc Blank                    durationMillis = duration.getMillis();
1840e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank                } catch (DateException e) {
18418d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    // We'll use the default in this case
18428d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
1843c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                ics.writeTag("DTEND" + vCalendarDateSuffix,
184479268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                        millisToEasDateTime(
1845c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                                startTime + durationMillis, vCalendarTimeZone, !allDayEvent));
1846c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1847c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1848dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            String location = null;
18498d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (entityValues.containsKey(Events.EVENT_LOCATION)) {
1850dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                location = entityValues.getAsString(Events.EVENT_LOCATION);
1851dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                ics.writeTag("LOCATION", location);
1852dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            }
1853dafc866120dac68fabbddcc9943e3995894c5244Marc Blank
185404c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik            String sequence = entityValues.getAsString(SYNC_VERSION);
1855dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            if (sequence == null) {
1856dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                sequence = "0";
18578d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
18585c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
185946e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            // We'll use 0 to mean a meeting invitation
18605c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            int titleId = 0;
18615c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            switch (messageFlag) {
18625c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_INVITE:
186346e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank                    if (!sequence.equals("0")) {
1864dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                        titleId = R.string.meeting_updated;
1865dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                    }
18665c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
18675c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_ACCEPT:
18685c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    titleId = R.string.meeting_accepted;
18695c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
18705c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_DECLINE:
18715c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    titleId = R.string.meeting_declined;
18725c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
18735c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
18745c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    titleId = R.string.meeting_tentative;
18755c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
187630d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank                case Message.FLAG_OUTGOING_MEETING_CANCEL:
187730d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank                    titleId = R.string.meeting_canceled;
187830d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank                    break;
18795c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            }
1880dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            Resources resources = context.getResources();
18818d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            String title = entityValues.getAsString(Events.TITLE);
18825c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            if (title == null) {
18835c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                title = "";
18848d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
18855c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            ics.writeTag("SUMMARY", title);
188646e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            // For meeting invitations just use the title
188746e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            if (titleId == 0) {
188846e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank                msg.mSubject = title;
188946e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            } else {
189046e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank                // Otherwise, use the additional text
1891dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                msg.mSubject = resources.getString(titleId, title);
189230d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank            }
189300702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank
189400702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            // Build the text for the message, starting with an initial line describing the
189500702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            // exception (if this is one)
189600702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            StringBuilder sb = new StringBuilder();
1897f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            if (isException && !isReply) {
189800702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                // Add the line, depending on whether this is a cancellation or update
189904e4cdadbe23670fd99d21ffb88af40ef77d69aeJay Shrauner                // isException indicates this key is present
190000702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                Date date = new Date(entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
190100702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                String dateString = DateFormat.getDateInstance().format(date);
190200702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                if (titleId == R.string.meeting_canceled) {
190300702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                    sb.append(resources.getString(R.string.exception_cancel, dateString));
190400702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                } else {
190500702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                    sb.append(resources.getString(R.string.exception_updated, dateString));
190600702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                }
190700702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                sb.append("\n\n");
190800702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            }
190900702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            String text =
191000702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                CalendarUtilities.buildMessageTextFromEntityValues(context, entityValues, sb);
191100702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank
191200702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            if (text.length() > 0) {
191300702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                ics.writeTag("DESCRIPTION", text);
191400702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            }
191500702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            // And store the message text
191600702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            msg.mText = text;
1917f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            if (!isReply) {
19185c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                if (entityValues.containsKey(Events.ALL_DAY)) {
19195c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
19205c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    ics.writeTag("X-MICROSOFT-CDO-ALLDAYEVENT", ade == 0 ? "FALSE" : "TRUE");
19215c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                }
1922c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
19235c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                String rrule = entityValues.getAsString(Events.RRULE);
19245c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                if (rrule != null) {
19255c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    ics.writeTag("RRULE", rrule);
19265c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                }
19275c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
192800702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                // If we decide to send alarm information in the meeting request ics file,
192900702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                // handle it here by looping through the subvalues
1930c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1931c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1932332d08cc2fc37d9936a73e3a120125c60b0587d1Marc Blank            // Handle attendee data here; determine "to" list and add ATTENDEE tags to ics
19338d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            String organizerName = null;
19348d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            String organizerEmail = null;
19358d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ArrayList<Address> toList = new ArrayList<Address>();
19368d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            for (NamedContentValues ncv: subValues) {
19378d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                Uri ncvUri = ncv.uri;
19388d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                ContentValues ncvValues = ncv.values;
19398d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                if (ncvUri.equals(Attendees.CONTENT_URI)) {
194020a434b03d719d53b82e2628759210670f1e51a9Jay Shrauner                    final Integer relationship =
19418d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
194220a434b03d719d53b82e2628759210670f1e51a9Jay Shrauner                    final String attendeeEmail =
194320a434b03d719d53b82e2628759210670f1e51a9Jay Shrauner                        ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
19448d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    // If there's no relationship, we can't create this for EAS
19458d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    // Similarly, we need an attendee email for each invitee
194620a434b03d719d53b82e2628759210670f1e51a9Jay Shrauner                    if (relationship != null && !TextUtils.isEmpty(attendeeEmail)) {
19478d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        // Organizer isn't among attendees in EAS
19488d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
19498d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                            organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
195020a434b03d719d53b82e2628759210670f1e51a9Jay Shrauner                            organizerEmail = attendeeEmail;
19518d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                            continue;
1952c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                        }
19538d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
195424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
195524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        // If we only want to send to the specifiedAttendee, eliminate others here
195624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        if ((specifiedAttendee != null) &&
195724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                                !attendeeEmail.equalsIgnoreCase(specifiedAttendee)) {
195824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                            continue;
1959c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                        }
196024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
196124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        addAttendeeToMessage(ics, toList, attendeeName, attendeeEmail, messageFlag,
196224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                                account);
1963c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                    }
1964c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                }
1965c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1966c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
196724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            // Manually add the specifiedAttendee if he wasn't added in the Attendees loop
196824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (toList.isEmpty() && (specifiedAttendee != null)) {
196924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                addAttendeeToMessage(ics, toList, null, specifiedAttendee, messageFlag, account);
197024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
197124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
19728d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            // Create the organizer tag for ical
19738d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (organizerEmail != null) {
19748d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                String icalTag = "ORGANIZER";
19758d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // We should be able to find this, assuming the Email is the user's email
19768d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // TODO Find this in the account
19778d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                if (organizerName != null) {
1978bc0c8c1523fc0bc42eb82dba6c2977492e441c03Makoto Onuki                    icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(organizerName);
19798d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
19808d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                ics.writeTag(icalTag, "MAILTO:" + organizerEmail);
1981f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank                if (isReply) {
19828d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    toList.add(organizerName == null ? new Address(organizerEmail) :
19838d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        new Address(organizerEmail, organizerName));
19848d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
1985c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1986c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
198724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            // If we have no "to" list, we're done
198824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (toList.isEmpty()) return null;
1989601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank
19908d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            // Write out the "to" list
19918d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            Address[] toArray = new Address[toList.size()];
19928d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            int i = 0;
19938d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            for (Address address: toList) {
19948d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                toArray[i++] = address;
19958d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
1996e89ec9216167fa7246ae268f2b2062e0a93621bfJames Lemieux            msg.mTo = Address.toHeader(toArray);
19978d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank
1998820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            ics.writeTag("CLASS", "PUBLIC");
1999dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            ics.writeTag("STATUS", (messageFlag == Message.FLAG_OUTGOING_MEETING_CANCEL) ?
2000dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                    "CANCELLED" : "CONFIRMED");
2001820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            ics.writeTag("TRANSP", "OPAQUE"); // What Exchange uses
2002820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            ics.writeTag("PRIORITY", "5");  // 1 to 9, 5 = medium
2003dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            ics.writeTag("SEQUENCE", sequence);
20048d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("END", "VEVENT");
20058d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("END", "VCALENDAR");
20065c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
20075c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // Create the ics attachment using the "content" field
20085c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            Attachment att = new Attachment();
2009bc0c8c1523fc0bc42eb82dba6c2977492e441c03Makoto Onuki            att.mContentBytes = ics.getBytes();
20105c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            att.mMimeType = "text/calendar; method=" + method;
20115c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            att.mFileName = "invite.ics";
2012e54b75dc4f638e594e9b97e3b4ed8829fbc9b521Makoto Onuki            att.mSize = att.mContentBytes.length;
20135c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // We don't send content-disposition with this attachment
2014b4d217e5170ae397d741e95308d98e80d0c2f637Marc Blank            att.mFlags = Attachment.FLAG_ICS_ALTERNATIVE_PART;
20155c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
20165c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // Add the attachment to the message
20175c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            msg.mAttachments = new ArrayList<Attachment>();
20185c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            msg.mAttachments.add(att);
20195c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        } catch (IOException e) {
2020942b7d73f2f5b3d6c651e39463e615fe6902a910Scott Kennedy            LogUtils.w(TAG, "IOException in createMessageForEntity");
20218d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            return null;
2022c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        }
2023c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
2024c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        // Return the new Message to caller
2025c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        return msg;
2026c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    }
2027c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
2028c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    /**
20296989716b639d274a98141674556ac9402be32ebeRoboErik     * Create a Message for an Event that can be retrieved from CalendarProvider
20306989716b639d274a98141674556ac9402be32ebeRoboErik     * by its unique id
20316989716b639d274a98141674556ac9402be32ebeRoboErik     *
2032c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param cr a content resolver that can be used to query for the Event
2033c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param eventId the unique id of the Event
20346989716b639d274a98141674556ac9402be32ebeRoboErik     * @param messageFlag the Message.FLAG_XXX constant indicating the type of
20356989716b639d274a98141674556ac9402be32ebeRoboErik     *            email to be sent
20366989716b639d274a98141674556ac9402be32ebeRoboErik     * @param the unique id of this Event, or null if it can be retrieved from
20376989716b639d274a98141674556ac9402be32ebeRoboErik     *            the Event
2038c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param the user's account
20396989716b639d274a98141674556ac9402be32ebeRoboErik     * @param requireAddressees if true (the default), no Message is returned if
20406989716b639d274a98141674556ac9402be32ebeRoboErik     *            there aren't any addressees; if false, return the Message
20416989716b639d274a98141674556ac9402be32ebeRoboErik     *            regardless (addressees will be filled in later)
2042c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @return a Message with many fields pre-filled (more later)
2043c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     */
20445c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank    static public EmailContent.Message createMessageForEventId(Context context, long eventId,
2045b31070f7484eeeb15c4bce89dbc61388d05d0bfcYu Ping Hu            int messageFlag, String uid, Account account) {
2046601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank        return createMessageForEventId(context, eventId, messageFlag, uid, account,
20476989716b639d274a98141674556ac9402be32ebeRoboErik                null /* specifiedAttendee */);
2048601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    }
2049601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank
2050601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    static public EmailContent.Message createMessageForEventId(Context context, long eventId,
2051b31070f7484eeeb15c4bce89dbc61388d05d0bfcYu Ping Hu            int messageFlag, String uid, Account account, String specifiedAttendee) {
2052185060cbeb39dc4539fbc0c72a865d8ec8d12979Jay Shrauner        final ContentResolver cr = context.getContentResolver();
2053185060cbeb39dc4539fbc0c72a865d8ec8d12979Jay Shrauner        final Cursor cursor = cr.query(ContentUris.withAppendedId(
2054185060cbeb39dc4539fbc0c72a865d8ec8d12979Jay Shrauner                        Events.CONTENT_URI, eventId), null, null, null, null);
2055185060cbeb39dc4539fbc0c72a865d8ec8d12979Jay Shrauner        if (cursor == null) {
2056185060cbeb39dc4539fbc0c72a865d8ec8d12979Jay Shrauner            return null;
2057185060cbeb39dc4539fbc0c72a865d8ec8d12979Jay Shrauner        }
2058185060cbeb39dc4539fbc0c72a865d8ec8d12979Jay Shrauner        final EntityIterator eventIterator = EventsEntity.newEntityIterator(cursor, cr);
2059c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        try {
2060c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            while (eventIterator.hasNext()) {
2061c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                Entity entity = eventIterator.next();
2062601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank                return createMessageForEntity(context, entity, messageFlag, uid, account,
206324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        specifiedAttendee);
2064c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
2065c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        } finally {
2066c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            eventIterator.close();
2067c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        }
2068c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        return null;
2069c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    }
2070393208ab154d18a876842777781ab153d34a0281Marc Blank
2071393208ab154d18a876842777781ab153d34a0281Marc Blank    /**
2072393208ab154d18a876842777781ab153d34a0281Marc Blank     * Return a boolean value for an integer ContentValues column
2073393208ab154d18a876842777781ab153d34a0281Marc Blank     * @param values a ContentValues object
2074393208ab154d18a876842777781ab153d34a0281Marc Blank     * @param columnName the name of a column to be found in the ContentValues
2075393208ab154d18a876842777781ab153d34a0281Marc Blank     * @return a boolean representation of the value of columnName in values; null and 0 = false,
2076393208ab154d18a876842777781ab153d34a0281Marc Blank     * other integers = true
2077393208ab154d18a876842777781ab153d34a0281Marc Blank     */
2078393208ab154d18a876842777781ab153d34a0281Marc Blank    static public boolean getIntegerValueAsBoolean(ContentValues values, String columnName) {
2079393208ab154d18a876842777781ab153d34a0281Marc Blank        Integer intValue = values.getAsInteger(columnName);
2080393208ab154d18a876842777781ab153d34a0281Marc Blank        return (intValue != null && intValue != 0);
2081393208ab154d18a876842777781ab153d34a0281Marc Blank    }
20828e26c42accbaf72eff6694173496aba0e6aa37f6Mihai Preda}
2083