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;
2777110d3a646dd691d84abd0b1e083385c1418ac5Marc Blankimport android.net.Uri;
28c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blankimport android.os.RemoteException;
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;
36f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blankimport android.util.Log;
375862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3860306788d571f234883d9833a97216c40289d0cfMichael Chanimport com.android.calendarcommon2.DateException;
3960306788d571f234883d9833a97216c40289d0cfMichael Chanimport com.android.calendarcommon2.Duration;
40f352bc9f29cacc61b195069e48d5c8b868660694Marc Blankimport com.android.emailcommon.Logging;
4160df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.mail.Address;
4260df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.Account;
4360df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.EmailContent;
4460df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.EmailContent.Attachment;
4560df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.EmailContent.Message;
4660df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.Mailbox;
4760df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.service.AccountServiceProxy;
4860df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.utility.Utility;
4960df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.Eas;
5060df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.EasSyncService;
5160df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.ExchangeService;
5260df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.R;
5360df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.adapter.Serializer;
5460df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.adapter.Tags;
55b931f82fa44c2e26e2645c0d5fde9eef3e666efdPaul Westbrookimport com.android.internal.util.ArrayUtils;
5660df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.google.common.annotations.VisibleForTesting;
5760df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank
5814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blankimport java.io.IOException;
59dafc866120dac68fabbddcc9943e3995894c5244Marc Blankimport java.text.DateFormat;
60c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blankimport java.util.ArrayList;
615862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.Calendar;
625862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.Date;
635862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.GregorianCalendar;
645862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.HashMap;
655862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.TimeZone;
665862a85e17e81866ca82a9905577931947fbd44eMarc Blank
675862a85e17e81866ca82a9905577931947fbd44eMarc Blankpublic class CalendarUtilities {
6804c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik
695862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // NOTE: Most definitions in this class are have package visibility for testing purposes
705862a85e17e81866ca82a9905577931947fbd44eMarc Blank    private static final String TAG = "CalendarUtility";
715862a85e17e81866ca82a9905577931947fbd44eMarc Blank
725862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Time related convenience constants, in milliseconds
735862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int SECONDS = 1000;
745862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MINUTES = SECONDS*60;
755862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int HOURS = MINUTES*60;
76377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static final long DAYS = HOURS*24;
775862a85e17e81866ca82a9905577931947fbd44eMarc Blank
782c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // We want to find a time zone whose DST info is accurate to one minute
792c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static final int STANDARD_DST_PRECISION = MINUTES;
802c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // If we can't find one, we'll try a more lenient standard (this is better than guessing a
812c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // time zone, which is what we otherwise do).  Note that this specifically addresses an issue
822c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // seen in some time zones sent by MS Exchange in which the start and end hour differ
832c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // for no apparent reason
842c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static final int LENIENT_DST_PRECISION = 4*HOURS;
852c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank
8604c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik    private static final String SYNC_VERSION = Events.SYNC_DATA4;
875862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // NOTE All Microsoft data structures are little endian
885862a85e17e81866ca82a9905577931947fbd44eMarc Blank
895862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // The following constants relate to standard Microsoft data sizes
905862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx
915862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_LONG_SIZE = 4;
925862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_WCHAR_SIZE = 2;
935862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_WORD_SIZE = 2;
945862a85e17e81866ca82a9905577931947fbd44eMarc Blank
955862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // The following constants relate to Microsoft's SYSTEMTIME structure
965862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4
975862a85e17e81866ca82a9905577931947fbd44eMarc Blank
985862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE;
995862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE;
1005862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE;
1015862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE;
1025862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE;
1035862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE;
1045862a85e17e81866ca82a9905577931947fbd44eMarc Blank    //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE;
1055862a85e17e81866ca82a9905577931947fbd44eMarc Blank    //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE;
1065862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE;
1075862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1085862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure
1095862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx
1107b873db28bda10f98f3782b7b161cb66117034d7Marc Blank    static final int MSFT_TIME_ZONE_STRING_SIZE = 32;
1117b873db28bda10f98f3782b7b161cb66117034d7Marc Blank
1125862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0;
1135862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET =
1145862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE;
1155862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET =
1167b873db28bda10f98f3782b7b161cb66117034d7Marc Blank        MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*MSFT_TIME_ZONE_STRING_SIZE);
1175862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET =
1185862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
1195862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET =
1205862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE;
1215862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET =
1227b873db28bda10f98f3782b7b161cb66117034d7Marc Blank        MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*MSFT_TIME_ZONE_STRING_SIZE);
1235862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET =
1245862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
1255862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_SIZE =
1265862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE;
1275862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1285862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // TimeZone cache; we parse/decode as little as possible, because the process is quite slow
1295862a85e17e81866ca82a9905577931947fbd44eMarc Blank    private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>();
130377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    // TZI string cache; we keep around our encoded TimeZoneInformation strings
131377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    private static HashMap<TimeZone, String> sTziStringCache = new HashMap<TimeZone, String>();
1325862a85e17e81866ca82a9905577931947fbd44eMarc Blank
133270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
134937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    // Default, Popup
13580a8e57ce2dc2695ed6f35599d326090e4ad9faeRoboErik    private static final String ALLOWED_REMINDER_TYPES = "0,1";
136937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    // None, required, optional
137937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    private static final String ALLOWED_ATTENDEE_TYPES = "0,1,2";
138937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    // Busy, free, tentative
139937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    private static final String ALLOWED_AVAILABILITIES = "0,1,2";
140270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank
1415862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // There is no type 4 (thus, the "")
1425862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final String[] sTypeToFreq =
1435862a85e17e81866ca82a9905577931947fbd44eMarc Blank        new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"};
1445862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1455862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final String[] sDayTokens =
1465862a85e17e81866ca82a9905577931947fbd44eMarc Blank        new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
1475862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1485862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final String[] sTwoCharacterNumbers =
1495862a85e17e81866ca82a9905577931947fbd44eMarc Blank        new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"};
1505862a85e17e81866ca82a9905577931947fbd44eMarc Blank
151f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    // Bits used in EAS recurrences for days of the week
152f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_SUNDAY = 1<<0;
153f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_MONDAY = 1<<1;
154f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_TUESDAY = 1<<2;
155f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_WEDNESDAY = 1<<3;
156f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_THURSDAY = 1<<4;
157f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_FRIDAY = 1<<5;
158f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_SATURDAY = 1<<6;
159f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_WEEKDAYS =
160f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        EAS_MONDAY | EAS_TUESDAY | EAS_WEDNESDAY | EAS_THURSDAY | EAS_FRIDAY;
161f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_WEEKENDS = EAS_SATURDAY | EAS_SUNDAY;
162f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank
163377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR);
164377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT");
165377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
166bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    private static final String ICALENDAR_ATTENDEE = "ATTENDEE;ROLE=REQ-PARTICIPANT";
167bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_CANCEL = ICALENDAR_ATTENDEE;
168bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_INVITE =
169bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=NEEDS-ACTION;RSVP=TRUE";
170bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_ACCEPT =
171bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=ACCEPTED";
172bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_DECLINE =
173bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=DECLINED";
174bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_TENTATIVE =
175bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=TENTATIVE";
176c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
177e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // Note that these constants apply to Calendar items
178e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // For future reference: MeetingRequest data can also include free/busy information, but the
179e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // constants for these four options in MeetingRequest data have different values!
180e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // See [MS-ASCAL] 2.2.2.8 for Calendar BusyStatus
181e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // See [MS-EMAIL] 2.2.2.34 for MeetingRequest BusyStatus
182e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_FREE = 0;
183e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_TENTATIVE = 1;
184e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_BUSY = 2;
185e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_OUT_OF_OFFICE = 3;
186dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank
18765d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    // Note that these constants apply to Calendar items, and are used in EAS 14+
18865d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    // See [MS-ASCAL] 2.2.2.22 for Calendar ResponseType
18965d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_NONE = 0;
19065d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_ORGANIZER = 1;
19165d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_TENTATIVE = 2;
19265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_ACCEPTED = 3;
19365d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_DECLINED = 4;
19465d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_NOT_RESPONDED = 5;
19565d022dc43e4461e86fd7bc143591f542b07428bMarc Blank
1965862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Return a 4-byte long from a byte array (little endian)
1975862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static int getLong(byte[] bytes, int offset) {
1985862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) |
1995862a85e17e81866ca82a9905577931947fbd44eMarc Blank        ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24);
2005862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2015862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2025862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Put a 4-byte long into a byte array (little endian)
2035862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static void setLong(byte[] bytes, int offset, int value) {
2045862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) (value & 0xFF);
2055862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) ((value >> 8) & 0xFF);
2065862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) ((value >> 16) & 0xFF);
2075862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset] = (byte) ((value >> 24) & 0xFF);
2085862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2095862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2105862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Return a 2-byte word from a byte array (little endian)
2115862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static int getWord(byte[] bytes, int offset) {
2125862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8);
2135862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2145862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2155862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Put a 2-byte word into a byte array (little endian)
2165862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static void setWord(byte[] bytes, int offset, int value) {
2175862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) (value & 0xFF);
2185862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset] = (byte) ((value >> 8) & 0xFF);
2195862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2205862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2217b873db28bda10f98f3782b7b161cb66117034d7Marc Blank    static String getString(byte[] bytes, int offset, int size) {
2227b873db28bda10f98f3782b7b161cb66117034d7Marc Blank        StringBuilder sb = new StringBuilder();
2237b873db28bda10f98f3782b7b161cb66117034d7Marc Blank        for (int i = 0; i < size; i++) {
2247b873db28bda10f98f3782b7b161cb66117034d7Marc Blank            int ch = bytes[offset + i];
2257b873db28bda10f98f3782b7b161cb66117034d7Marc Blank            if (ch == 0) {
2267b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                break;
2277b873db28bda10f98f3782b7b161cb66117034d7Marc Blank            } else {
2287b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                sb.append((char)ch);
2297b873db28bda10f98f3782b7b161cb66117034d7Marc Blank            }
2307b873db28bda10f98f3782b7b161cb66117034d7Marc Blank        }
2317b873db28bda10f98f3782b7b161cb66117034d7Marc Blank        return sb.toString();
2327b873db28bda10f98f3782b7b161cb66117034d7Marc Blank    }
2337b873db28bda10f98f3782b7b161cb66117034d7Marc Blank
2345862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Internal structure for storing a time zone date from a SYSTEMTIME structure
2355862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // This date represents either the start or the end time for DST
2365862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static class TimeZoneDate {
2375862a85e17e81866ca82a9905577931947fbd44eMarc Blank        String year;
2385862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int month;
2395862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int dayOfWeek;
2405862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int day;
2415862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int time;
2425862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int hour;
2435862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int minute;
2445862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2455862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2464868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank    @VisibleForTesting
2474868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank    static void clearTimeZoneCache() {
2484868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank        sTimeZoneCache.clear();
2494868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank    }
2504868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank
251820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static void putRuleIntoTimeZoneInformation(byte[] bytes, int offset, RRule rrule, int hour,
252820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            int minute) {
253820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // MSFT months are 1 based, same as RRule
254820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, rrule.month);
255820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // MSFT day of week starts w/ Sunday = 0; RRule starts w/ Sunday = 1
256820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, rrule.dayOfWeek - 1);
257820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // 5 means "last" in MSFT land; for RRule, it's -1
258820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, rrule.week < 0 ? 5 : rrule.week);
259820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Turn hours/minutes into ms from midnight (per TimeZone)
260820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, hour);
261820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, minute);
262820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
263820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
26410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    // Write a transition time into SYSTEMTIME data (via an offset into a byte array)
26510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    static void putTransitionMillisIntoSystemTime(byte[] bytes, int offset, long millis) {
266377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
267377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // Round to the next highest minute; we always write seconds as zero
268377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        cal.setTimeInMillis(millis + 30*SECONDS);
269377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
270377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // MSFT months are 1 based; TimeZone is 0 based
271377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, cal.get(Calendar.MONTH) + 1);
272377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
273377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, cal.get(Calendar.DAY_OF_WEEK) - 1);
274377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
275377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // Get the "day" in TimeZone format
276377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        int wom = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
277377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // 5 means "last" in MSFT land; for TimeZone, it's -1
278377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, wom < 0 ? 5 : wom);
279377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
280377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // Turn hours/minutes into ms from midnight (per TimeZone)
28110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, getTrueTransitionHour(cal));
28210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, getTrueTransitionMinute(cal));
283377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     }
284377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
2855862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset
2865862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) {
2875862a85e17e81866ca82a9905577931947fbd44eMarc Blank        TimeZoneDate tzd = new TimeZoneDate();
2885862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2895862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT year is an int; TimeZone is a String
2905862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR);
2915862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.year = Integer.toString(num);
2925862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2935862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT month = 0 means no daylight time
2945862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT months are 1 based; TimeZone is 0 based
2955862a85e17e81866ca82a9905577931947fbd44eMarc Blank        num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH);
2965862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (num == 0) {
2975862a85e17e81866ca82a9905577931947fbd44eMarc Blank            return null;
2985862a85e17e81866ca82a9905577931947fbd44eMarc Blank        } else {
2995862a85e17e81866ca82a9905577931947fbd44eMarc Blank            tzd.month = num -1;
3005862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
3015862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3025862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
3035862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1;
3045862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3055862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Get the "day" in TimeZone format
3065862a85e17e81866ca82a9905577931947fbd44eMarc Blank        num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY);
3075862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // 5 means "last" in MSFT land; for TimeZone, it's -1
3085862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (num == 5) {
3095862a85e17e81866ca82a9905577931947fbd44eMarc Blank            tzd.day = -1;
3105862a85e17e81866ca82a9905577931947fbd44eMarc Blank        } else {
3115862a85e17e81866ca82a9905577931947fbd44eMarc Blank            tzd.day = num;
3125862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
3135862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3145862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Turn hours/minutes into ms from midnight (per TimeZone)
3155862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR);
3165862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.hour = hour;
3175862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE);
3185862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.minute = minute;
3195862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.time = (hour*HOURS) + (minute*MINUTES);
3205862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3215862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return tzd;
3225862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
3235862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3245862a85e17e81866ca82a9905577931947fbd44eMarc Blank    /**
3255862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * Build a GregorianCalendar, based on a time zone and TimeZoneDate.
3265862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @param timeZone the time zone we're checking
3275862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @param tzd the TimeZoneDate we're interested in
3285862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @return a GregorianCalendar with the given time zone and date
3295862a85e17e81866ca82a9905577931947fbd44eMarc Blank     */
33079268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static long getMillisAtTimeZoneDateTransition(TimeZone timeZone, TimeZoneDate tzd) {
3315862a85e17e81866ca82a9905577931947fbd44eMarc Blank        GregorianCalendar testCalendar = new GregorianCalendar(timeZone);
332377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        testCalendar.set(GregorianCalendar.YEAR, sCurrentYear);
3335862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.MONTH, tzd.month);
3345862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek);
3355862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day);
3365862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour);
3375862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.MINUTE, tzd.minute);
33879268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        testCalendar.set(GregorianCalendar.SECOND, 0);
33979268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        return testCalendar.getTimeInMillis();
3405862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
3415862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3425862a85e17e81866ca82a9905577931947fbd44eMarc Blank    /**
343820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Return a GregorianCalendar representing the first standard/daylight transition between a
344820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * start time and an end time in the given time zone
345820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz a TimeZone the time zone in which we're looking for transitions
346377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param startTime the start time for the test
347377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param endTime the end time for the test
348377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param startInDaylightTime whether daylight time is in effect at the startTime
349820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return a GregorianCalendar representing the transition or null if none
350377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
35179268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static GregorianCalendar findTransitionDate(TimeZone tz, long startTime,
35210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            long endTime, boolean startInDaylightTime) {
353377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        long startingEndTime = endTime;
354377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        Date date = null;
35510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank
356820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll keep splitting the difference until we're within a minute
357377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        while ((endTime - startTime) > MINUTES) {
358377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            long checkTime = ((startTime + endTime) / 2) + 1;
359377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            date = new Date(checkTime);
36010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            boolean inDaylightTime = tz.inDaylightTime(date);
36110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            if (inDaylightTime != startInDaylightTime) {
362377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                endTime = checkTime;
363377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            } else {
364377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                startTime = checkTime;
365377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
366377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
367820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
368820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // If these are the same, we're really messed up; return null
369377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (endTime == startingEndTime) {
370820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return null;
371377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
372820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
37310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        // Set up our calendar and return it
374820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar calendar = new GregorianCalendar(tz);
37510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        calendar.setTimeInMillis(startTime);
376820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return calendar;
377377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
378377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
379377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
380377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Return a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
381377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * that might be found in an Event; use cached result, if possible
382377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param tz the TimeZone
383377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
384377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
385377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static public String timeZoneToTziString(TimeZone tz) {
386377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        String tziString = sTziStringCache.get(tz);
387377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (tziString != null) {
388377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            if (Eas.USER_LOG) {
389385a0be662509754e687bcfa9813208b050bf951Marc Blank                ExchangeService.log(TAG, "TZI string for " + tz.getDisplayName() +
390385a0be662509754e687bcfa9813208b050bf951Marc Blank                        " found in cache.");
391377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
392377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            return tziString;
393377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
394377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        tziString = timeZoneToTziStringImpl(tz);
395377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sTziStringCache.put(tz, tziString);
396377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return tziString;
397377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
398377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
399377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
400820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * A class for storing RRULE information.  The RRULE members can be accessed individually or
401820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * an RRULE string can be created with toString()
402820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
403820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static class RRule {
404820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        static final int RRULE_NONE = 0;
405820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        static final int RRULE_DAY_WEEK = 1;
406820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        static final int RRULE_DATE = 2;
407820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
408820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int type;
409820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int dayOfWeek;
410820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int week;
411820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int month;
412820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int date;
413820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
414820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        /**
415820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * Create an RRULE based on month and date
416820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _month the month (1 = JAN, 12 = DEC)
417820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _date the date in the month (1-31)
418820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         */
419820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule(int _month, int _date) {
420820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            type = RRULE_DATE;
421820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            month = _month;
422820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            date = _date;
423820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
424820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
425820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        /**
426820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * Create an RRULE based on month, day of week, and week #
427820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _month the month (1 = JAN, 12 = DEC)
428820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _dayOfWeek the day of the week (1 = SU, 7 = SA)
429820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _week the week in the month (1-5 or -1 for last)
430820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         */
431820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule(int _month, int _dayOfWeek, int _week) {
432820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            type = RRULE_DAY_WEEK;
433820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            month = _month;
434820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            dayOfWeek = _dayOfWeek;
435820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            week = _week;
436820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
437820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
438820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        @Override
439820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        public String toString() {
440820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (type == RRULE_DAY_WEEK) {
441820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return "FREQ=YEARLY;BYMONTH=" + month + ";BYDAY=" + week +
442820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    sDayTokens[dayOfWeek - 1];
443820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
444820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return "FREQ=YEARLY;BYMONTH=" + month + ";BYMONTHDAY=" + date;
445820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
446820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank       }
447820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
448820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
449820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
450820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Generate an RRULE string for an array of GregorianCalendars, if possible.  For now, we are
451820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * only looking for rules based on the same date in a month or a specific instance of a day of
452820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the week in a month (e.g. 2nd Tuesday or last Friday).  Indeed, these are the only kinds of
453820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * rules used in the current tzinfo database.
454820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param calendars an array of GregorianCalendar, set to a series of transition times in
455820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * consecutive years starting with the current year
456820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return an RRULE or null if none could be inferred from the calendars
457820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
45879268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static RRule inferRRuleFromCalendars(GregorianCalendar[] calendars) {
459820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Let's see if we can make a rule about these
460820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar calendar = calendars[0];
461820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (calendar == null) return null;
462820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int month = calendar.get(Calendar.MONTH);
463820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int date = calendar.get(Calendar.DAY_OF_MONTH);
464820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
465820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int week = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
466820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int maxWeek = calendar.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
467820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        boolean dateRule = false;
468820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        boolean dayOfWeekRule = false;
469820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        for (int i = 1; i < calendars.length; i++) {
470820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar cal = calendars[i];
471820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal == null) return null;
472820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // If it's not the same month, there's no rule
473820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal.get(Calendar.MONTH) != month) {
474820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return null;
475820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (dayOfWeek == cal.get(Calendar.DAY_OF_WEEK)) {
476820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Ok, it seems to be the same day of the week
477820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (dateRule) {
478820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    return null;
479820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
480820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                dayOfWeekRule = true;
481820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                int thisWeek = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
482820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (week != thisWeek) {
483820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    if (week < 0 || week == maxWeek) {
484820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        int thisMaxWeek = cal.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
485820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        if (thisWeek == thisMaxWeek) {
486820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            // We'll use -1 (i.e. last) week
487820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            week = -1;
488820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            continue;
489820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        }
490820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    }
491820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    return null;
492820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
493820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (date == cal.get(Calendar.DAY_OF_MONTH)) {
494820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Maybe the same day of the month?
495820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (dayOfWeekRule) {
496820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    return null;
497820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
498820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                dateRule = true;
499820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
500820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return null;
501820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
502820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
503820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
504820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (dateRule) {
505820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return new RRule(month + 1, date);
506820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
507820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // sDayTokens is 0 based (SU = 0); Calendar days of week are 1 based (SU = 1)
508820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // iCalendar months are 1 based; Calendar months are 0 based
509820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // So we adjust these when building the string
510820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return new RRule(month + 1, dayOfWeek, week);
511820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
512820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
513820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
514820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Generate an rfc2445 utcOffset from minutes offset from GMT
515820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * These look like +0800 or -0100
516820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param offsetMinutes minutes offset from GMT (east is positive, west is negative
517820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return a utcOffset
518820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
51979268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static String utcOffsetString(int offsetMinutes) {
520820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        StringBuilder sb = new StringBuilder();
521820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int hours = offsetMinutes / 60;
522820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hours < 0) {
523820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('-');
524820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            hours = 0 - hours;
525820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        } else {
526820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('+');
527820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
528820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int minutes = offsetMinutes % 60;
529820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hours < 10) {
530820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('0');
531820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
532820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(hours);
533820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (minutes < 10) {
534820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('0');
535820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
536820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(minutes);
537820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return sb.toString();
538820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
539820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
540820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
541820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Fill the passed in GregorianCalendars arrays with DST transition information for this and
542820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the following years (based on the length of the arrays)
543820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz the time zone
544820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param toDaylightCalendars an array of GregorianCalendars, one for each year, representing
545820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the transition to daylight time
546820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param toStandardCalendars an array of GregorianCalendars, one for each year, representing
547820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the transition to standard time
548820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return true if transitions could be found for all years, false otherwise
549820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
550820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static boolean getDSTCalendars(TimeZone tz, GregorianCalendar[] toDaylightCalendars,
551820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar[] toStandardCalendars) {
552820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll use the length of the arrays to determine how many years to check
553820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int maxYears = toDaylightCalendars.length;
554820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (toStandardCalendars.length != maxYears) {
555820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return false;
556820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
557820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Get the transitions for this year and the next few years
558820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        for (int i = 0; i < maxYears; i++) {
559820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar cal = new GregorianCalendar(tz);
560820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            cal.set(sCurrentYear + i, Calendar.JANUARY, 1, 0, 0, 0);
561820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            long startTime = cal.getTimeInMillis();
562820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Calculate end of year; no need to be insanely precise
563820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            long endOfYearTime = startTime + (365*DAYS) + (DAYS>>2);
564820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            Date date = new Date(startTime);
565820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            boolean startInDaylightTime = tz.inDaylightTime(date);
566820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Find the first transition, and store
567820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            cal = findTransitionDate(tz, startTime, endOfYearTime, startInDaylightTime);
568820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal == null) {
569820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return false;
570820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (startInDaylightTime) {
571820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toStandardCalendars[i] = cal;
572820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
573820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toDaylightCalendars[i] = cal;
574820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
575820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Find the second transition, and store
576820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            cal = findTransitionDate(tz, startTime, endOfYearTime, !startInDaylightTime);
577820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal == null) {
578820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return false;
579820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (startInDaylightTime) {
580820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toDaylightCalendars[i] = cal;
581820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
582820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toStandardCalendars[i] = cal;
583820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
584820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
585820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return true;
586820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
587820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
588820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
589820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Write out the STANDARD block of VTIMEZONE and end the VTIMEZONE
590820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param writer the SimpleIcsWriter we're using
591820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz the time zone
592820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param offsetString the offset string in VTIMEZONE format (e.g. +0800)
593820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @throws IOException
594820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
595820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static private void writeNoDST(SimpleIcsWriter writer, TimeZone tz, String offsetString)
596820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            throws IOException {
597820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "STANDARD");
598820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETFROM", offsetString);
599820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETTO", offsetString);
600820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Might as well use start of epoch for start date
601820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("DTSTART", millisToEasDateTime(0L));
602820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "STANDARD");
603820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "VTIMEZONE");
604820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
605820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
606820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /** Write a VTIMEZONE block for a given TimeZone into a SimpleIcsWriter
607820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz the TimeZone to be used in the conversion
608820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param writer the SimpleIcsWriter to be used
609820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @throws IOException
610820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
61179268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static void timeZoneToVTimezone(TimeZone tz, SimpleIcsWriter writer)
612820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            throws IOException {
613820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll use these regardless of whether there's DST in this time zone or not
614820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int rawOffsetMinutes = tz.getRawOffset() / MINUTES;
615820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        String standardOffsetString = utcOffsetString(rawOffsetMinutes);
616820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
617820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Preamble for all of our VTIMEZONEs
618820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "VTIMEZONE");
619820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZID", tz.getID());
620820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("X-LIC-LOCATION", tz.getDisplayName());
621820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
622820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Simplest case is no daylight time
623820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (!tz.useDaylightTime()) {
624820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writeNoDST(writer, tz, standardOffsetString);
625820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return;
626820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
627820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
628820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int maxYears = 3;
629820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[maxYears];
630820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar[] toStandardCalendars = new GregorianCalendar[maxYears];
631820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (!getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) {
632820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writeNoDST(writer, tz, standardOffsetString);
633820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return;
634820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
635820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Try to find a rule to cover these yeras
636820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars);
637820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule standardRule = inferRRuleFromCalendars(toStandardCalendars);
638820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        String daylightOffsetString =
639820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            utcOffsetString(rawOffsetMinutes + (tz.getDSTSavings() / MINUTES));
640820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll use RRULE's if we found both
641820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Otherwise we write the first as DTSTART and the others as RDATE
642820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        boolean hasRule = daylightRule != null && standardRule != null;
643820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
644820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Write the DAYLIGHT block
645820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "DAYLIGHT");
646820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETFROM", standardOffsetString);
647820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETTO", daylightOffsetString);
648820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("DTSTART",
64910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                transitionMillisToVCalendarTime(
65010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        toDaylightCalendars[0].getTimeInMillis(), tz, true));
651820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hasRule) {
652820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writer.writeTag("RRULE", daylightRule.toString());
653820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        } else {
654820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            for (int i = 1; i < maxYears; i++) {
65510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                writer.writeTag("RDATE", transitionMillisToVCalendarTime(
656820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        toDaylightCalendars[i].getTimeInMillis(), tz, true));
657820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
658820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
659820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "DAYLIGHT");
660820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Write the STANDARD block
661820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "STANDARD");
662820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETFROM", daylightOffsetString);
663820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETTO", standardOffsetString);
664820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("DTSTART",
66510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                transitionMillisToVCalendarTime(
66610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        toStandardCalendars[0].getTimeInMillis(), tz, false));
667820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hasRule) {
668820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writer.writeTag("RRULE", standardRule.toString());
669820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        } else {
670820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            for (int i = 1; i < maxYears; i++) {
67110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                writer.writeTag("RDATE", transitionMillisToVCalendarTime(
672820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        toStandardCalendars[i].getTimeInMillis(), tz, true));
673820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
674820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
675820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "STANDARD");
676820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // And we're done
677820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "VTIMEZONE");
678820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
679820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
680820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
681820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Find the next transition to occur (i.e. after the current date/time)
682820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param transitions calendars representing transitions to/from DST
683820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return millis for the first transition after the current date/time
684820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
68579268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static long findNextTransition(long startingMillis, GregorianCalendar[] transitions) {
686820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        for (GregorianCalendar transition: transitions) {
687820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            long transitionMillis = transition.getTimeInMillis();
688820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (transitionMillis > startingMillis) {
689820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return transitionMillis;
690820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
691820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
692820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return 0;
693820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
694820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
695820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
696377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Calculate the Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
697377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * that might be found in an Event.  Since the internal representation of the TimeZone is hidden
698377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * from us we'll find the DST transitions and build the structure from that information
699377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param tz the TimeZone
700377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
701377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
70279268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static String timeZoneToTziStringImpl(TimeZone tz) {
703377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        String tziString;
704377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE];
705377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        int standardBias = - tz.getRawOffset();
706377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        standardBias /= 60*SECONDS;
707377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias);
708820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // If this time zone has daylight savings time, we need to do more work
709377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (tz.useDaylightTime()) {
710820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[3];
711820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar[] toStandardCalendars = new GregorianCalendar[3];
712820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // See if we can get transitions for a few years; if not, we can't generate DST info
713820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // for this time zone
714820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) {
715820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Try to find a rule to cover these years
716820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars);
717820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                RRule standardRule = inferRRuleFromCalendars(toStandardCalendars);
718820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if ((daylightRule != null) && (daylightRule.type == RRule.RRULE_DAY_WEEK) &&
719820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        (standardRule != null) && (standardRule.type == RRule.RRULE_DAY_WEEK)) {
720820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // We need both rules and they have to be DAY/WEEK type
721820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // Write month, day of week, week, hour, minute
722820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET,
723820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            standardRule,
72410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionHour(toStandardCalendars[0]),
72510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionMinute(toStandardCalendars[0]));
726820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET,
727820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            daylightRule,
72810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionHour(toDaylightCalendars[0]),
72910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionMinute(toDaylightCalendars[0]));
730820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                } else {
731820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // If there's no rule, we'll use the first transition to standard/to daylight
732820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // And indicate that it's just for this year...
733820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    long now = System.currentTimeMillis();
734820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    long standardTransition = findNextTransition(now, toStandardCalendars);
735820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    long daylightTransition = findNextTransition(now, toDaylightCalendars);
736820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // If we can't find transitions, we can't do DST
737820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    if (standardTransition != 0 && daylightTransition != 0) {
73810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        putTransitionMillisIntoSystemTime(tziBytes,
73910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                                MSFT_TIME_ZONE_STANDARD_DATE_OFFSET, standardTransition);
74010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        putTransitionMillisIntoSystemTime(tziBytes,
74110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                                MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET, daylightTransition);
742820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    }
743820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
744377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
745820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            int dstOffset = tz.getDSTSavings();
746820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES);
747377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
7487e85a8d17b2b0c80bd04306db5a698ac3b91deaaMakoto Onuki        byte[] tziEncodedBytes = Base64.encode(tziBytes, Base64.NO_WRAP);
749377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        tziString = new String(tziEncodedBytes);
750377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return tziString;
751377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
752377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
753377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
7545862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * Given a String as directly read from EAS, returns a TimeZone corresponding to that String
7555862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @param timeZoneString the String read from the server
7562c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * @param precision the number of milliseconds of precision in TimeZone determination
7575862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @return the TimeZone, or TimeZone.getDefault() if not found
7585862a85e17e81866ca82a9905577931947fbd44eMarc Blank     */
7592c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    @VisibleForTesting
7602c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static TimeZone tziStringToTimeZone(String timeZoneString, int precision) {
7615862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // If we have this time zone cached, use that value and return
7625862a85e17e81866ca82a9905577931947fbd44eMarc Blank        TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
7635862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (timeZone != null) {
7645862a85e17e81866ca82a9905577931947fbd44eMarc Blank            if (Eas.USER_LOG) {
7655bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                ExchangeService.log(TAG, " Using cached TimeZone " + timeZone.getID());
766377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
767377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        } else {
7682c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank            timeZone = tziStringToTimeZoneImpl(timeZoneString, precision);
769377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            if (timeZone == null) {
770377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                // If we don't find a match, we just return the current TimeZone.  In theory, this
771377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                // shouldn't be happening...
772385a0be662509754e687bcfa9813208b050bf951Marc Blank                ExchangeService.alwaysLog("TimeZone not found using default: " + timeZoneString);
773377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                timeZone = TimeZone.getDefault();
7745862a85e17e81866ca82a9905577931947fbd44eMarc Blank            }
775377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            sTimeZoneCache.put(timeZoneString, timeZone);
7765862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
777377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return timeZone;
778377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
7795862a85e17e81866ca82a9905577931947fbd44eMarc Blank
780377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
7812c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * The standard entry to EAS time zone conversion, using one minute as the precision
7822c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     */
7832c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static public TimeZone tziStringToTimeZone(String timeZoneString) {
7842c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank        return tziStringToTimeZone(timeZoneString, MINUTES);
7852c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    }
7862c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank
7872c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    /**
788377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Given a String as directly read from EAS, tries to find a TimeZone in the database of all
7892c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * time zones that corresponds to that String.  If the test time zone string includes DST and
7902c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * we don't find a match, and we're using standard precision, we try again with lenient
7912c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * precision, which is a bit better than guessing
792377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param timeZoneString the String read from the server
79379268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank     * @return the TimeZone, or null if not found
794377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
7952c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static TimeZone tziStringToTimeZoneImpl(String timeZoneString, int precision) {
796377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        TimeZone timeZone = null;
7975862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // First, we need to decode the base64 string
7987e85a8d17b2b0c80bd04306db5a698ac3b91deaaMakoto Onuki        byte[] timeZoneBytes = Base64.decode(timeZoneString, Base64.DEFAULT);
7995862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8005862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms
8015862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // but EAS gives us minutes, so do the conversion.  Note that EAS is the bias that's added
8025862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so
8035862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // we need to change the sign
8045862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES;
8055862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8065862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return
8075862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // the default time zone
8085862a85e17e81866ca82a9905577931947fbd44eMarc Blank        String[] zoneIds = TimeZone.getAvailableIDs(bias);
8095862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (zoneIds.length > 0) {
8105862a85e17e81866ca82a9905577931947fbd44eMarc Blank            // Try to find an existing TimeZone from the data provided by EAS
8115862a85e17e81866ca82a9905577931947fbd44eMarc Blank            // We start by pulling out the date that standard time begins
8125862a85e17e81866ca82a9905577931947fbd44eMarc Blank            TimeZoneDate dstEnd =
8135862a85e17e81866ca82a9905577931947fbd44eMarc Blank                getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET);
8145862a85e17e81866ca82a9905577931947fbd44eMarc Blank            if (dstEnd == null) {
8155bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // If the default time zone is a match
8165bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                TimeZone defaultTimeZone = TimeZone.getDefault();
8175bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                if (!defaultTimeZone.useDaylightTime() &&
818b931f82fa44c2e26e2645c0d5fde9eef3e666efdPaul Westbrook                        ArrayUtils.contains(zoneIds, defaultTimeZone.getID())) {
8195bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    if (Eas.USER_LOG) {
8205bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        ExchangeService.log(TAG, "TimeZone without DST found to be default: " +
8215bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                                defaultTimeZone.getID());
8225bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    }
8235bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    return defaultTimeZone;
8245bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                }
8255862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // In this case, there is no daylight savings time, so the only interesting data
8265bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // for possible matches is the offset and DST availability; we'll take the first
8275bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // match for those
8285bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                for (String zoneId: zoneIds) {
8295bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    timeZone = TimeZone.getTimeZone(zoneId);
8305bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    if (!timeZone.useDaylightTime()) {
8315bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        if (Eas.USER_LOG) {
8325bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                            ExchangeService.log(TAG, "TimeZone without DST found by offset: " +
8335bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                                    timeZone.getID());
8345bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        }
8355bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        return timeZone;
8365bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    }
8375862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
8385bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // None found, return null
8395bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                return null;
8405862a85e17e81866ca82a9905577931947fbd44eMarc Blank            } else {
841377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes,
8425862a85e17e81866ca82a9905577931947fbd44eMarc Blank                        MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET);
8435862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // See comment above for bias...
8445862a85e17e81866ca82a9905577931947fbd44eMarc Blank                long dstSavings =
845377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                    -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * MINUTES;
8465862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8475862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // We'll go through each time zone to find one with the same DST transitions and
8485862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // savings length
8495862a85e17e81866ca82a9905577931947fbd44eMarc Blank                for (String zoneId: zoneIds) {
8505862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // Get the TimeZone using the zoneId
8515862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    timeZone = TimeZone.getTimeZone(zoneId);
8525862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8535862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // Our strategy here is to check just before and just after the transitions
8545862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // and see whether the check for daylight time matches the expectation
8555862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // If both transitions match, then we have a match for the offset and start/end
8565862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // of dst.  That's the best we can do for now, since there's no other info
8575862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // provided by EAS (i.e. we can't get dynamic transitions, etc.)
8585862a85e17e81866ca82a9905577931947fbd44eMarc Blank
85979268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // Check one minute before and after DST start transition
86079268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    long millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstStart);
8612c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    Date before = new Date(millisAtTransition - precision);
8622c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    Date after = new Date(millisAtTransition + precision);
8635862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (timeZone.inDaylightTime(before)) continue;
8645862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (!timeZone.inDaylightTime(after)) continue;
8655862a85e17e81866ca82a9905577931947fbd44eMarc Blank
86679268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // Check one minute before and after DST end transition
86779268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstEnd);
86879268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // Note that we need to subtract an extra hour here, because we end up with
86979268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // gaining an hour in the transition BACK to standard time
8702c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    before = new Date(millisAtTransition - (dstSavings + precision));
8712c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    after = new Date(millisAtTransition + precision);
8725862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (!timeZone.inDaylightTime(before)) continue;
8735862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (timeZone.inDaylightTime(after)) continue;
8745862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8755862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // Check that the savings are the same
8765862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (dstSavings != timeZone.getDSTSavings()) continue;
87779268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    return timeZone;
8785862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
8792c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                boolean lenient = false;
8807b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                boolean name = false;
8812c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                if ((dstStart.hour != dstEnd.hour) && (precision == STANDARD_DST_PRECISION)) {
8822c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    timeZone = tziStringToTimeZoneImpl(timeZoneString, LENIENT_DST_PRECISION);
8832c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    lenient = true;
8842c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                } else {
8857b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                    // We can't find a time zone match, so our last attempt is to see if there's
8867b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                    // a valid time zone name in the TZI; if not we'll just take the first TZ with
8877b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                    // a matching offset (which is likely wrong, but ... what else is there to do)
8887b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                    String tzName = getString(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_NAME_OFFSET,
8897b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                            MSFT_TIME_ZONE_STRING_SIZE);
8907b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                    if (!tzName.isEmpty()) {
8917b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                        TimeZone tz = TimeZone.getTimeZone(tzName);
8927b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                        if (tz != null) {
8937b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                            timeZone = tz;
8947b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                            name = true;
8957b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                        } else {
8967b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                            timeZone = TimeZone.getTimeZone(zoneIds[0]);
8977b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                        }
8987b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                    } else {
8997b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                        timeZone = TimeZone.getTimeZone(zoneIds[0]);
9007b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                    }
9012c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                }
902270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                if (Eas.USER_LOG) {
903385a0be662509754e687bcfa9813208b050bf951Marc Blank                    ExchangeService.log(TAG,
9042c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                            "No TimeZone with correct DST settings; using " +
9057b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                            (name ? "name" : (lenient ? "lenient" : "first")) + ": " +
9067b873db28bda10f98f3782b7b161cb66117034d7Marc Blank                                    timeZone.getID());
907270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                }
908270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                return timeZone;
9095862a85e17e81866ca82a9905577931947fbd44eMarc Blank            }
9105862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
91179268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        return null;
9125862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
9135862a85e17e81866ca82a9905577931947fbd44eMarc Blank
9145c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank    static public String convertEmailDateTimeToCalendarDateTime(String date) {
9155c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        // Format for email date strings is 2010-02-23T16:00:00.000Z
91679268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        // Format for calendar date strings is 20100223T160000Z
9175c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank       return date.substring(0, 4) + date.substring(5, 7) + date.substring(8, 13) +
9185c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank           date.substring(14, 16) + date.substring(17, 19) + 'Z';
9195c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank    }
9205c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
921377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static String formatTwo(int num) {
922377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (num <= 12) {
923377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            return sTwoCharacterNumbers[num];
924377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        } else
925377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            return Integer.toString(num);
926377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
92714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
928377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
929377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Generate an EAS formatted date/time string based on GMT. See below for details.
930377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
931377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static public String millisToEasDateTime(long millis) {
932c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank        return millisToEasDateTime(millis, sGmtTimeZone, true);
933377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
9345862a85e17e81866ca82a9905577931947fbd44eMarc Blank
935377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
936e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank     * Generate a birthday string from a GregorianCalendar set appropriately; the format of this
937e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank     * string is YYYY-MM-DD
938e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank     * @param cal the calendar
939e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank     * @return the birthday string
940e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank     */
941e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank    static public String calendarToBirthdayString(GregorianCalendar cal) {
942e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        StringBuilder sb = new StringBuilder();
943e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        sb.append(cal.get(Calendar.YEAR));
944e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        sb.append('-');
945e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
946e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        sb.append('-');
947e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
948e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        return sb.toString();
949e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank    }
950e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank
951e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank    /**
952c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * Generate an EAS formatted local date/time string from a time and a time zone. If the final
953c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * argument is false, only a date will be returned (e.g. 20100331)
954377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param millis a time in milliseconds
955377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param tz a time zone
956c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * @param withTime if the time is to be included in the string
957c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * @return an EAS formatted string indicating the date (and time) in the given time zone
958377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
959c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank    static public String millisToEasDateTime(long millis, TimeZone tz, boolean withTime) {
960377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        StringBuilder sb = new StringBuilder();
961377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        GregorianCalendar cal = new GregorianCalendar(tz);
962377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        cal.setTimeInMillis(millis);
963377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sb.append(cal.get(Calendar.YEAR));
964377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
965377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
966c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank        if (withTime) {
967c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append('T');
968c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY)));
969c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append(formatTwo(cal.get(Calendar.MINUTE)));
970c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append(formatTwo(cal.get(Calendar.SECOND)));
971c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            if (tz == sGmtTimeZone) {
972c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                sb.append('Z');
973c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            }
974820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
975820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return sb.toString();
976820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
977820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
978820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
97910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Return the true minute at which a transition occurs
98010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Our transition time should be the in the minute BEFORE the transition
98110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * If this minute is 59, set minute to 0 and increment the hour
98210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * NOTE: We don't want to add a minute and retrieve minute/hour from the Calendar, because
98310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Calendar time will itself be influenced by the transition!  So adding 1 minute to
98410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * 01:59 (assume PST->PDT) will become 03:00, which isn't what we want (we want 02:00)
98510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     *
98610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @param calendar the calendar holding the transition date/time
98710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @return the true minute of the transition
98810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     */
98979268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static int getTrueTransitionMinute(GregorianCalendar calendar) {
99010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        int minute = calendar.get(Calendar.MINUTE);
99110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        if (minute == 59) {
99210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            minute = 0;
99310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        }
99410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        return minute;
99510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    }
99610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank
99710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    /**
99810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Return the true hour at which a transition occurs
99910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * See description for getTrueTransitionMinute, above
100010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @param calendar the calendar holding the transition date/time
100110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @return the true hour of the transition
100210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     */
100379268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static int getTrueTransitionHour(GregorianCalendar calendar) {
100410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        int hour = calendar.get(Calendar.HOUR_OF_DAY);
100510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        hour++;
100610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        if (hour == 24) {
100710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            hour = 0;
100810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        }
100910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        return hour;
101010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    }
101110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank
101210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    /**
101310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Generate a date/time string suitable for VTIMEZONE from a transition time in millis
101410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * The format is YYYYMMDDTHHMMSS
101510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @param millis a transition time in milliseconds
1016820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz a time zone
1017820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param dst whether we're entering daylight time
1018820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
101979268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static String transitionMillisToVCalendarTime(long millis, TimeZone tz, boolean dst) {
1020820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        StringBuilder sb = new StringBuilder();
1021820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar cal = new GregorianCalendar(tz);
1022820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        cal.setTimeInMillis(millis);
1023820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(cal.get(Calendar.YEAR));
1024820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
1025820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
1026820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append('T');
102710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        sb.append(formatTwo(getTrueTransitionHour(cal)));
102810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        sb.append(formatTwo(getTrueTransitionMinute(cal)));
102910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        sb.append(formatTwo(0));
1030377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return sb.toString();
1031377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
10325862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1033cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank    /**
1034270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * Returns a UTC calendar with year/month/day from local calendar and h/m/s/ms = 0
1035270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @param time the time in seconds of an all-day event in local time
1036270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @return the time in seconds in UTC
1037cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank     */
1038270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    static public long getUtcAllDayCalendarTime(long time, TimeZone localTimeZone) {
1039270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        return transposeAllDayTime(time, localTimeZone, UTC_TIMEZONE);
1040270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    }
1041270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank
1042270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    /**
1043270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * Returns a local calendar with year/month/day from UTC calendar and h/m/s/ms = 0
1044270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @param time the time in seconds of an all-day event in UTC
1045270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @return the time in seconds in local time
1046270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     */
1047270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    static public long getLocalAllDayCalendarTime(long time, TimeZone localTimeZone) {
1048270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        return transposeAllDayTime(time, UTC_TIMEZONE, localTimeZone);
1049270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    }
1050270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank
1051270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    static private long transposeAllDayTime(long time, TimeZone fromTimeZone,
1052270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank            TimeZone toTimeZone) {
1053270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        GregorianCalendar fromCalendar = new GregorianCalendar(fromTimeZone);
1054270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        fromCalendar.setTimeInMillis(time);
1055270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        GregorianCalendar toCalendar = new GregorianCalendar(toTimeZone);
1056cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank        // Set this calendar with correct year, month, and day, but zero hour, minute, and seconds
1057270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        toCalendar.set(fromCalendar.get(GregorianCalendar.YEAR),
1058270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                fromCalendar.get(GregorianCalendar.MONTH),
1059270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                fromCalendar.get(GregorianCalendar.DATE), 0, 0, 0);
10603baaee079e644467cf18c9b250ac30485f9c54e0Marc Blank        toCalendar.set(GregorianCalendar.MILLISECOND, 0);
1061270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        return toCalendar.getTimeInMillis();
1062cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank    }
1063cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank
1064377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static void addByDay(StringBuilder rrule, int dow, int wom) {
10655862a85e17e81866ca82a9905577931947fbd44eMarc Blank        rrule.append(";BYDAY=");
10665862a85e17e81866ca82a9905577931947fbd44eMarc Blank        boolean addComma = false;
10675862a85e17e81866ca82a9905577931947fbd44eMarc Blank        for (int i = 0; i < 7; i++) {
10685862a85e17e81866ca82a9905577931947fbd44eMarc Blank            if ((dow & 1) == 1) {
10695862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (addComma) {
10705862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    rrule.append(',');
10715862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
10725862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (wom > 0) {
10735862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // 5 = last week -> -1
10745862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // So -1SU = last sunday
10755862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    rrule.append(wom == 5 ? -1 : wom);
10765862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
10775862a85e17e81866ca82a9905577931947fbd44eMarc Blank                rrule.append(sDayTokens[i]);
10785862a85e17e81866ca82a9905577931947fbd44eMarc Blank                addComma = true;
10795862a85e17e81866ca82a9905577931947fbd44eMarc Blank            }
10805862a85e17e81866ca82a9905577931947fbd44eMarc Blank            dow >>= 1;
10815862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
10825862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
10835862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1084f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    static void addBySetpos(StringBuilder rrule, int dow, int wom) {
1085f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        // Indicate the days, but don't use wom in this case (it's used in the BYSETPOS);
1086f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        addByDay(rrule, dow, 0);
1087f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        rrule.append(";BYSETPOS=");
1088f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        rrule.append(wom == 5 ? "-1" : wom);
1089f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    }
1090f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank
10915862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static void addByMonthDay(StringBuilder rrule, int dom) {
10925862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // 127 means last day of the month
10935862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (dom == 127) {
10945862a85e17e81866ca82a9905577931947fbd44eMarc Blank            dom = -1;
10955862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
10965862a85e17e81866ca82a9905577931947fbd44eMarc Blank        rrule.append(";BYMONTHDAY=" + dom);
10975862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
10985862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1099f35b67cef20189c12a1a387dedf200eb30089725Marc Blank    /**
1100f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     * Generate the String version of the EAS integer for a given BYDAY value in an rrule
1101f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     * @param dow the BYDAY value of the rrule
1102f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     * @return the String version of the EAS value of these days
1103f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     */
110414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    static String generateEasDayOfWeek(String dow) {
1105f35b67cef20189c12a1a387dedf200eb30089725Marc Blank        int bits = 0;
110614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int bit = 1;
110714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        for (String token: sDayTokens) {
1108f35b67cef20189c12a1a387dedf200eb30089725Marc Blank            // If we can find the day in the dow String, add the bit to our bits value
1109f35b67cef20189c12a1a387dedf200eb30089725Marc Blank            if (dow.indexOf(token) >= 0) {
1110f35b67cef20189c12a1a387dedf200eb30089725Marc Blank                bits |= bit;
111114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            }
1112f35b67cef20189c12a1a387dedf200eb30089725Marc Blank            bit <<= 1;
111314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        }
1114f35b67cef20189c12a1a387dedf200eb30089725Marc Blank        return Integer.toString(bits);
111514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    }
111614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
1117377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
1118377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Extract the value of a token in an RRULE string
1119377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param rrule an RRULE string
1120377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param token a token to look for in the RRULE
1121377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return the value of that token
1122377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
112314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    static String tokenFromRrule(String rrule, String token) {
112414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int start = rrule.indexOf(token);
112514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        if (start < 0) return null;
112614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int len = rrule.length();
112714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        start += token.length();
112814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int end = start;
112914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        char c;
113014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        do {
113114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            c = rrule.charAt(end++);
1132b129a5f1b340ae6362397685c407099ceae8d9e9Marc Blank            if ((c == ';') || (end == len)) {
113314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                if (end == len) end++;
113414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                return rrule.substring(start, end -1);
113514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            }
1136377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        } while (true);
113714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    }
113814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
113914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    /**
114021c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank     * Reformat an RRULE style UNTIL to an EAS style until
114121c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank     */
114260df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank    @VisibleForTesting
114321c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    static String recurrenceUntilToEasUntil(String until) {
114460df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // Get a calendar in our local time zone
114560df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        GregorianCalendar localCalendar = new GregorianCalendar(TimeZone.getDefault());
114660df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // Set the time per GMT time in the 'until'
114760df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        localCalendar.setTimeInMillis(Utility.parseDateTimeToMillis(until));
114821c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        StringBuilder sb = new StringBuilder();
114960df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // Build a string with local year/month/date
115060df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        sb.append(localCalendar.get(Calendar.YEAR));
115160df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        sb.append(formatTwo(localCalendar.get(Calendar.MONTH) + 1));
115260df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        sb.append(formatTwo(localCalendar.get(Calendar.DAY_OF_MONTH)));
115360df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // EAS ignores the time in 'until'; go figure
1154e3e9ef55a2afd2c121e1f214a8fa34fced3bac38Marc Blank        sb.append("T000000Z");
115521c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        return sb.toString();
115621c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    }
115721c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank
115821c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    /**
1159f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank     * Convenience method to add "count", "interval", and "until" to an EAS calendar stream
1160f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank     * According to EAS docs, OCCURRENCES must always come before INTERVAL
116121c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank     */
1162f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank    static private void addCountIntervalAndUntil(String rrule, Serializer s) throws IOException {
1163f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        String count = tokenFromRrule(rrule, "COUNT=");
1164f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (count != null) {
1165f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            s.data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, count);
1166f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
1167f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        String interval = tokenFromRrule(rrule, "INTERVAL=");
1168f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (interval != null) {
1169f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, interval);
1170f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
117121c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        String until = tokenFromRrule(rrule, "UNTIL=");
117221c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        if (until != null) {
117321c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank            s.data(Tags.CALENDAR_RECURRENCE_UNTIL, recurrenceUntilToEasUntil(until));
117421c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        }
117521c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    }
117621c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank
1177f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank    static private void addByDay(String byDay, Serializer s) throws IOException {
1178f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        // This can be 1WE (1st Wednesday) or -1FR (last Friday)
1179f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        int weekOfMonth = byDay.charAt(0);
1180f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        String bareByDay;
1181f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (weekOfMonth == '-') {
1182f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            // -1 is the only legal case (last week) Use "5" for EAS
1183f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            weekOfMonth = 5;
1184f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            bareByDay = byDay.substring(2);
1185f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        } else {
1186f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            weekOfMonth = weekOfMonth - '0';
1187f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            bareByDay = byDay.substring(1);
1188f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
1189f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
1190f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
1191f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank    }
1192f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank
1193f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    static private void addByDaySetpos(String byDay, String bySetpos, Serializer s)
1194f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            throws IOException {
1195f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        int weekOfMonth = bySetpos.charAt(0);
1196f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        if (weekOfMonth == '-') {
1197f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            // -1 is the only legal case (last week) Use "5" for EAS
1198f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            weekOfMonth = 5;
1199f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        } else {
1200f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            weekOfMonth = weekOfMonth - '0';
1201f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        }
1202f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
1203f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
1204f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    }
1205f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank
120621c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    /**
120714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * Write recurrence information to EAS based on the RRULE in CalendarProvider
120814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @param rrule the RRULE, from CalendarProvider
120914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @param startTime, the DTSTART of this Event
121014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @param s the Serializer we're using to write WBXML data
121114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @throws IOException
121214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     */
121314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    // NOTE: For the moment, we're only parsing recurrence types that are supported by the
1214c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    // Calendar app UI, which is a subset of possible recurrence types
121514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    // This code must be updated when the Calendar adds new functionality
121614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    static public void recurrenceFromRrule(String rrule, long startTime, Serializer s)
121779268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank            throws IOException {
121888683d3ebf9720ee648220dd6884e200d5e87038Marc Blank        if (Eas.USER_LOG) {
1219385a0be662509754e687bcfa9813208b050bf951Marc Blank            ExchangeService.log(TAG, "RRULE: " + rrule);
122088683d3ebf9720ee648220dd6884e200d5e87038Marc Blank        }
122114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        String freq = tokenFromRrule(rrule, "FREQ=");
122214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        // If there's no FREQ=X, then we don't write a recurrence
122314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        // Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the
122414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        // possibility of writing out a partial recurrence stanza
122514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        if (freq != null) {
122614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            if (freq.equals("DAILY")) {
122714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.start(Tags.CALENDAR_RECURRENCE);
122814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0");
1229f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                addCountIntervalAndUntil(rrule, s);
123014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.end();
123114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            } else if (freq.equals("WEEKLY")) {
123214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.start(Tags.CALENDAR_RECURRENCE);
123314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1");
123414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                // Requires a day of week (whereas RRULE does not)
1235f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                addCountIntervalAndUntil(rrule, s);
123614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byDay = tokenFromRrule(rrule, "BYDAY=");
123714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                if (byDay != null) {
123814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
1239f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    // Find week number (1-4 and 5 for last)
1240f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    if (byDay.startsWith("-1")) {
1241f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, "5");
1242f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    } else {
1243f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        char c = byDay.charAt(0);
1244f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        if (c >= '1' && c <= '4') {
1245f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                            s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, byDay.substring(0, 1));
1246f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        }
1247f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    }
124814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                }
124914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.end();
125014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            } else if (freq.equals("MONTHLY")) {
125114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
125214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                if (byMonthDay != null) {
125314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    s.start(Tags.CALENDAR_RECURRENCE);
1254f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    // Special case for last day of month
1255f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    if (byMonthDay == "-1") {
1256f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
1257f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        addCountIntervalAndUntil(rrule, s);
1258f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "127");
1259f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    } else {
1260f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        // The nth day of the month
1261f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
1262f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        addCountIntervalAndUntil(rrule, s);
1263f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
1264f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    }
126514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    s.end();
126614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                } else {
126714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    String byDay = tokenFromRrule(rrule, "BYDAY=");
1268f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    String bySetpos = tokenFromRrule(rrule, "BYSETPOS=");
126914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    if (byDay != null) {
127014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                        s.start(Tags.CALENDAR_RECURRENCE);
127114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
1272f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                        addCountIntervalAndUntil(rrule, s);
1273f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        if (bySetpos != null) {
1274f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                            addByDaySetpos(byDay, bySetpos, s);
1275f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        } else {
1276f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                            addByDay(byDay, s);
1277f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        }
127814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                        s.end();
127914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    }
128014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                }
128114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            } else if (freq.equals("YEARLY")) {
128214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byMonth = tokenFromRrule(rrule, "BYMONTH=");
128314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
1284f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                String byDay = tokenFromRrule(rrule, "BYDAY=");
1285f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                if (byMonth == null && byMonthDay == null) {
128614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    // Calculate the month and day from the startDate
128714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    GregorianCalendar cal = new GregorianCalendar();
128814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    cal.setTimeInMillis(startTime);
128914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    cal.setTimeZone(TimeZone.getDefault());
129014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1);
129114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
129214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                }
1293f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                if (byMonth != null && (byMonthDay != null || byDay != null)) {
1294f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.start(Tags.CALENDAR_RECURRENCE);
1295f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.data(Tags.CALENDAR_RECURRENCE_TYPE, byDay == null ? "5" : "6");
1296f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    addCountIntervalAndUntil(rrule, s);
1297f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
1298f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    // Note that both byMonthDay and byDay can't be true in a valid RRULE
1299f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    if (byMonthDay != null) {
1300f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
1301f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    } else {
1302f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                        addByDay(byDay, s);
1303f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    }
1304f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.end();
1305f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                }
1306377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
130714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        }
130814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    }
130914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
1310377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
1311377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Build an RRULE String from EAS recurrence information
1312377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param type the type of recurrence
1313377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param occurrences how many recurrences (instances)
1314377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param interval the interval between recurrences
1315377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param dow day of the week
1316377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param dom day of the month
1317377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param wom week of the month
1318377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param moy month of the year
1319377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param until the last recurrence time
1320377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return a valid RRULE String
1321377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
13225862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow,
13235862a85e17e81866ca82a9905577931947fbd44eMarc Blank            int dom, int wom, int moy, String until) {
13245862a85e17e81866ca82a9905577931947fbd44eMarc Blank        StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]);
13255862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // INTERVAL and COUNT
13265862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (occurrences > 0) {
13275862a85e17e81866ca82a9905577931947fbd44eMarc Blank            rrule.append(";COUNT=" + occurrences);
13285862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
1329f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (interval > 0) {
1330f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            rrule.append(";INTERVAL=" + interval);
1331f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
13325862a85e17e81866ca82a9905577931947fbd44eMarc Blank
13335862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Days, weeks, months, etc.
13345862a85e17e81866ca82a9905577931947fbd44eMarc Blank        switch(type) {
13355862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 0: // DAILY
13365862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 1: // WEEKLY
1337f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                if (dow > 0) addByDay(rrule, dow, wom);
13385862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
13395862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 2: // MONTHLY
13405862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (dom > 0) addByMonthDay(rrule, dom);
13415862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
13425862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 3: // MONTHLY (on the nth day)
1343f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                // 127 is a special case meaning "last day of the month"
1344f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                if (dow == 127) {
1345f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    rrule.append(";BYMONTHDAY=-1");
1346f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                // week 5 and dow = weekdays -> last weekday (need BYSETPOS)
1347e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank                } else if ((wom == 5 || wom == 1) && (dow == EAS_WEEKDAYS || dow == EAS_WEEKENDS)) {
1348f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    addBySetpos(rrule, dow, wom);
1349f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                } else if (dow > 0) addByDay(rrule, dow, wom);
13505862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
1351820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            case 5: // YEARLY (specific day)
13525862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (dom > 0) addByMonthDay(rrule, dom);
13535862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (moy > 0) {
13545862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    rrule.append(";BYMONTH=" + moy);
13555862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
13565862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
1357820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            case 6: // YEARLY
13585862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (dow > 0) addByDay(rrule, dow, wom);
1359820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (dom > 0) addByMonthDay(rrule, dom);
1360820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (moy > 0) {
1361820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    rrule.append(";BYMONTH=" + moy);
1362820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
13635862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
13645862a85e17e81866ca82a9905577931947fbd44eMarc Blank            default:
13655862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
13665862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
13675862a85e17e81866ca82a9905577931947fbd44eMarc Blank
13685862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // UNTIL comes last
13695862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (until != null) {
137021c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank            rrule.append(";UNTIL=" + until);
13715862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
13725862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1373f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        if (Eas.USER_LOG) {
1374f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            Log.d(Logging.LOG_TAG, "Created rrule: " + rrule);
1375f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        }
13765862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return rrule.toString();
13775862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
137877110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank
137977110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank    /**
138077110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * Create a Calendar in CalendarProvider to which synced Events will be linked
138177110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @param service the sync service requesting Calendar creation
138277110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @param account the account being synced
138377110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @param mailbox the Exchange mailbox for the calendar
138477110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @return the unique id of the Calendar
138577110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     */
138677110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank    static public long createCalendar(EasSyncService service, Account account, Mailbox mailbox) {
138777110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // Create a Calendar object
138877110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        ContentValues cv = new ContentValues();
138977110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // TODO How will this change if the user changes his account display name?
139004c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik        cv.put(Calendars.CALENDAR_DISPLAY_NAME, account.mDisplayName);
13919e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        cv.put(Calendars.ACCOUNT_NAME, account.mEmailAddress);
13929e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        cv.put(Calendars.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
139377110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        cv.put(Calendars.SYNC_EVENTS, 1);
1394443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.VISIBLE, 1);
13955a02f79c91df6df44f3c95742f61f2c25c464cc3Marc Blank        // Don't show attendee status if we're the organizer
1396443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.CAN_ORGANIZER_RESPOND, 0);
1397443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.CAN_MODIFY_TIME_ZONE, 0);
1398443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.MAX_REMINDERS, 1);
139980a8e57ce2dc2695ed6f35599d326090e4ad9faeRoboErik        cv.put(Calendars.ALLOWED_REMINDERS, ALLOWED_REMINDER_TYPES);
1400937af5abcbc1268f22a3058b00835c74ba20f116RoboErik        cv.put(Calendars.ALLOWED_ATTENDEE_TYPES, ALLOWED_ATTENDEE_TYPES);
1401937af5abcbc1268f22a3058b00835c74ba20f116RoboErik        cv.put(Calendars.ALLOWED_AVAILABILITY, ALLOWED_AVAILABILITIES);
14025a02f79c91df6df44f3c95742f61f2c25c464cc3Marc Blank
140377110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // TODO Coordinate account colors w/ Calendar, if possible
1404d1771c4473a98c032c95ff66fa816043e08976f1Todd Kennedy        int color = new AccountServiceProxy(service.mContext).getAccountColor(account.mId);
14059e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        cv.put(Calendars.CALENDAR_COLOR, color);
140604c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik        cv.put(Calendars.CALENDAR_TIME_ZONE, Time.getCurrentTimezone());
140704c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik        cv.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
140877110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        cv.put(Calendars.OWNER_ACCOUNT, account.mEmailAddress);
140977110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank
14106989716b639d274a98141674556ac9402be32ebeRoboErik        Uri uri = service.mContentResolver.insert(
14116989716b639d274a98141674556ac9402be32ebeRoboErik                asSyncAdapter(Calendars.CONTENT_URI, account.mEmailAddress,
14126989716b639d274a98141674556ac9402be32ebeRoboErik                        Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
141377110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // We save the id of the calendar into mSyncStatus
141477110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        if (uri != null) {
141577110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank            String stringId = uri.getPathSegments().get(1);
141677110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank            mailbox.mSyncStatus = stringId;
141777110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank            return Long.parseLong(stringId);
141877110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        }
141977110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        return -1;
142077110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank    }
1421c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
14226989716b639d274a98141674556ac9402be32ebeRoboErik    static Uri asSyncAdapter(Uri uri, String account, String accountType) {
14236989716b639d274a98141674556ac9402be32ebeRoboErik        return uri.buildUpon()
1424693ed7fdd5a7ec7af87d105b76267c78a8acc3dbRoboErik                .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,
1425693ed7fdd5a7ec7af87d105b76267c78a8acc3dbRoboErik                        "true")
14266989716b639d274a98141674556ac9402be32ebeRoboErik                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
14276989716b639d274a98141674556ac9402be32ebeRoboErik                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
14286989716b639d274a98141674556ac9402be32ebeRoboErik    }
14296989716b639d274a98141674556ac9402be32ebeRoboErik
1430b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank    /**
1431b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     * Return the uid for an event based on its globalObjId
1432b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     * @param globalObjId the base64 encoded String provided by EAS
1433b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     * @return the uid for the calendar event
1434b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     */
1435b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank    static public String getUidFromGlobalObjId(String globalObjId) {
1436b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        StringBuilder sb = new StringBuilder();
1437b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        // First get the decoded base64
1438b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        try {
1439b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            byte[] idBytes = Base64.decode(globalObjId, Base64.DEFAULT);
1440b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            String idString = new String(idBytes);
1441b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // If the base64 decoded string contains the magic substring: "vCal-Uid", then
1442b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // the actual uid is hidden within; the magic substring is never at the start of the
1443b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // decoded base64
1444b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            int index = idString.indexOf("vCal-Uid");
1445b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            if (index > 0) {
1446b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // The uid starts after "vCal-Uidxxxx", where xxxx are padding
1447b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // characters.  And it ends before the last character, which is ascii 0
1448b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                return idString.substring(index + 12, idString.length() - 1);
1449b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            } else {
1450b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // This is an EAS uid. Go through the bytes and write out the hex
1451b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // values as characters; this is what we'll need to pass back to EAS
1452b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // when responding to the invitation
1453b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                for (byte b: idBytes) {
1454b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                    Utility.byteToHex(sb, b);
1455b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                }
1456b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                return sb.toString();
1457b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            }
1458b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        } catch (RuntimeException e) {
1459b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // In the worst of cases (bad format, etc.), we can always return the input
1460b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            return globalObjId;
1461b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        }
1462b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank    }
1463b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank
1464dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    /**
1465dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * Get a selfAttendeeStatus from a busy status
1466dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * The default here is NONE (i.e. we don't know the status)
1467dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * Note that a busy status of FREE must mean NONE as well, since it can't mean declined
1468dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * (there would be no event)
1469dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @param busyStatus the busy status, from EAS
1470dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @return the corresponding value for selfAttendeeStatus
1471dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     */
1472edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank    static public int attendeeStatusFromBusyStatus(int busyStatus) {
1473edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank        int attendeeStatus;
1474dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        switch (busyStatus) {
1475dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case BUSY_STATUS_BUSY:
1476edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
1477dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1478dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case BUSY_STATUS_TENTATIVE:
1479edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
1480dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1481dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case BUSY_STATUS_FREE:
1482e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank            case BUSY_STATUS_OUT_OF_OFFICE:
1483dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            default:
1484edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
1485dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        }
1486edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank        return attendeeStatus;
1487dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    }
1488dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank
148965d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    /**
1490f9d3d43800dcb522a7c492e96d490eca9f120e43Marc Blank     * Get a selfAttendeeStatus from a response type (EAS 14+)
1491f9d3d43800dcb522a7c492e96d490eca9f120e43Marc Blank     * The default here is NONE (i.e. we don't know the status), though in theory this can't happen
1492f9d3d43800dcb522a7c492e96d490eca9f120e43Marc Blank     * @param busyStatus the response status, from EAS
149365d022dc43e4461e86fd7bc143591f542b07428bMarc Blank     * @return the corresponding value for selfAttendeeStatus
149465d022dc43e4461e86fd7bc143591f542b07428bMarc Blank     */
149565d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    static public int attendeeStatusFromResponseType(int responseType) {
149665d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        int attendeeStatus;
149765d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        switch (responseType) {
149865d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_NOT_RESPONDED:
149965d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
150065d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
150165d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_ACCEPTED:
150265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
150365d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
150465d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_TENTATIVE:
150565d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
150665d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
150765d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_DECLINED:
150865d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_DECLINED;
150965d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
151065d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            default:
151165d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
151265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        }
151365d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        return attendeeStatus;
151465d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    }
151565d022dc43e4461e86fd7bc143591f542b07428bMarc Blank
1516dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    /** Get a busy status from a selfAttendeeStatus
1517dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * The default here is BUSY
1518dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @param selfAttendeeStatus from CalendarProvider2
1519dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @return the corresponding value of busy status
1520dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     */
1521edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank    static public int busyStatusFromAttendeeStatus(int selfAttendeeStatus) {
1522dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        int busyStatus;
1523dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        switch (selfAttendeeStatus) {
1524dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_DECLINED:
1525dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_NONE:
1526dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_INVITED:
1527dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                busyStatus = BUSY_STATUS_FREE;
1528dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1529dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_TENTATIVE:
1530dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                busyStatus = BUSY_STATUS_TENTATIVE;
1531dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1532dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_ACCEPTED:
1533dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            default:
1534dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                busyStatus = BUSY_STATUS_BUSY;
1535dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1536dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        }
1537dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        return busyStatus;
1538dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    }
1539dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank
1540e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    /** Get a busy status from event availability
1541e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * The default here is TENTATIVE
1542e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * @param availability from CalendarProvider2
1543e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * @return the corresponding value of busy status
1544e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     */
1545e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    static public int busyStatusFromAvailability(int availability) {
1546e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        int busyStatus;
1547e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        switch (availability) {
1548e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case Events.AVAILABILITY_BUSY:
1549e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                busyStatus = BUSY_STATUS_BUSY;
1550e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1551e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case Events.AVAILABILITY_FREE:
1552e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                busyStatus = BUSY_STATUS_FREE;
1553e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1554e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case Events.AVAILABILITY_TENTATIVE:
1555e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            default:
1556e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                busyStatus = BUSY_STATUS_TENTATIVE;
1557e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1558e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        }
1559e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        return busyStatus;
1560e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    }
1561e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank
1562e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    /** Get an event availability from busy status
1563e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * The default here is TENTATIVE
1564e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * @param busyStatus from CalendarProvider2
1565e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     * @return the corresponding availability value
1566e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank     */
1567e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    static public int availabilityFromBusyStatus(int busyStatus) {
1568e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        int availability;
1569e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        switch (busyStatus) {
1570e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case BUSY_STATUS_BUSY:
1571e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                availability = Events.AVAILABILITY_BUSY;
1572e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1573e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case BUSY_STATUS_FREE:
1574e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                availability = Events.AVAILABILITY_FREE;
1575e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1576e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            case BUSY_STATUS_TENTATIVE:
1577e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank            default:
1578e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                availability = Events.AVAILABILITY_TENTATIVE;
1579e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank                break;
1580e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        }
1581e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank        return availability;
1582e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank    }
1583e3668322ce61d0cd8488e0c8fcdc8d067bfb769eMarc Blank
1584dafc866120dac68fabbddcc9943e3995894c5244Marc Blank    static public String buildMessageTextFromEntityValues(Context context,
1585dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            ContentValues entityValues, StringBuilder sb) {
1586dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        if (sb == null) {
1587dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            sb = new StringBuilder();
1588dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        }
1589dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        Resources resources = context.getResources();
1590dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // TODO: Add more detail to message text
1591dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // Right now, we're using.. When: Tuesday, March 5th at 2:00pm
1592dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // What we're missing is the duration and any recurrence information.  So this should be
1593dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // more like... When: Tuesdays, starting March 5th from 2:00pm - 3:00pm
1594dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // This would require code to build complex strings, and it will have to wait
15959ca8918b82221dd441293973ffb84d565a52993aMarc Blank        // For now, we'll just use the meeting_recurring string
1596f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank
1597f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        boolean allDayEvent = false;
1598f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        if (entityValues.containsKey(Events.ALL_DAY)) {
1599f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
1600f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            allDayEvent = (ade != null) && (ade == 1);
1601f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        }
1602f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        boolean recurringEvent = !entityValues.containsKey(Events.ORIGINAL_SYNC_ID) &&
1603f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            entityValues.containsKey(Events.RRULE);
1604f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank
1605f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        String dateTimeString;
1606f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        int res;
1607e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank        long startTime = entityValues.getAsLong(Events.DTSTART);
1608f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        if (allDayEvent) {
1609e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank            Date date = new Date(getLocalAllDayCalendarTime(startTime, TimeZone.getDefault()));
1610f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            dateTimeString = DateFormat.getDateInstance().format(date);
1611f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            res = recurringEvent ? R.string.meeting_allday_recurring : R.string.meeting_allday;
16129ca8918b82221dd441293973ffb84d565a52993aMarc Blank        } else {
1613e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank            dateTimeString = DateFormat.getDateTimeInstance().format(new Date(startTime));
1614f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            res = recurringEvent ? R.string.meeting_recurring : R.string.meeting_when;
16159ca8918b82221dd441293973ffb84d565a52993aMarc Blank        }
1616f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        sb.append(resources.getString(res, dateTimeString));
1617f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank
1618dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        String location = null;
1619dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        if (entityValues.containsKey(Events.EVENT_LOCATION)) {
1620dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            location = entityValues.getAsString(Events.EVENT_LOCATION);
1621efae936b117c9e4f3056d52fdbfe4d3f261483e5Marc Blank            if (!TextUtils.isEmpty(location)) {
1622dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                sb.append("\n");
1623dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                sb.append(resources.getString(R.string.meeting_where, location));
1624dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            }
1625dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        }
1626dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // If there's a description for this event, append it
1627dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        String desc = entityValues.getAsString(Events.DESCRIPTION);
1628dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        if (desc != null) {
1629dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            sb.append("\n--\n");
1630dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            sb.append(desc);
1631dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        }
1632dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        return sb.toString();
1633dafc866120dac68fabbddcc9943e3995894c5244Marc Blank    }
1634dafc866120dac68fabbddcc9943e3995894c5244Marc Blank
1635c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    /**
163624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * Add an attendee to the ics attachment and the to list of the Message being composed
163724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param ics the ics attachment writer
163824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param toList the list of addressees for this email
163924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param attendeeName the name of the attendee
164024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param attendeeEmail the email address of the attendee
164124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param messageFlag the flag indicating the action to be indicated by the message
164224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param account the sending account of the email
164324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     */
164424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank    static private void addAttendeeToMessage(SimpleIcsWriter ics, ArrayList<Address> toList,
164524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            String attendeeName, String attendeeEmail, int messageFlag, Account account) {
164624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank        if ((messageFlag & Message.FLAG_OUTGOING_MEETING_REQUEST_MASK) != 0) {
164724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            String icalTag = ICALENDAR_ATTENDEE_INVITE;
164824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if ((messageFlag & Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
164924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                icalTag = ICALENDAR_ATTENDEE_CANCEL;
165024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
165124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (attendeeName != null) {
165224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(attendeeName);
165324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
165424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
165524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            toList.add(attendeeName == null ? new Address(attendeeEmail) :
165624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                new Address(attendeeEmail, attendeeName));
165724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank        } else if (attendeeEmail.equalsIgnoreCase(account.mEmailAddress)) {
165824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            String icalTag = null;
165924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            switch (messageFlag) {
166024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                case Message.FLAG_OUTGOING_MEETING_ACCEPT:
166124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag = ICALENDAR_ATTENDEE_ACCEPT;
166224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    break;
166324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                case Message.FLAG_OUTGOING_MEETING_DECLINE:
166424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag = ICALENDAR_ATTENDEE_DECLINE;
166524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    break;
166624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
166724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag = ICALENDAR_ATTENDEE_TENTATIVE;
166824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    break;
166924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
167024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (icalTag != null) {
167124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                if (attendeeName != null) {
167224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag += ";CN="
167324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                            + SimpleIcsWriter.quoteParamValue(attendeeName);
167424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                }
167524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
167624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
167724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank        }
167824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank    }
167924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
168024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank    /**
1681c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * Create a Message for an (Event) Entity
1682c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param entity the Entity for the Event (as might be retrieved by CalendarProvider)
1683c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent
1684c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param the unique id of this Event, or null if it can be retrieved from the Event
1685c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param the user's account
1686c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @return a Message with many fields pre-filled (more later)
1687c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     */
1688c8e4352ea6cfa67f15140512e84af8ccede222d2Marc Blank    static public Message createMessageForEntity(Context context, Entity entity,
16895c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            int messageFlag, String uid, Account account) {
1690601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank        return createMessageForEntity(context, entity, messageFlag, uid, account,
169124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                null /*specifiedAttendee*/);
1692601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    }
1693601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank
1694601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    static public EmailContent.Message createMessageForEntity(Context context, Entity entity,
169524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            int messageFlag, String uid, Account account, String specifiedAttendee) {
1696c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        ContentValues entityValues = entity.getEntityValues();
16975c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        ArrayList<NamedContentValues> subValues = entity.getSubValues();
16989e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        boolean isException = entityValues.containsKey(Events.ORIGINAL_SYNC_ID);
1699f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank        boolean isReply = false;
1700c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1701c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        EmailContent.Message msg = new EmailContent.Message();
1702c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        msg.mFlags = messageFlag;
1703c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        msg.mTimeStamp = System.currentTimeMillis();
1704c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1705c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        String method;
1706f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank        if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE) != 0) {
1707c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            method = "REQUEST";
1708f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank        } else if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
1709f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            method = "CANCEL";
1710c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        } else {
1711c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            method = "REPLY";
1712f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            isReply = true;
1713c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        }
1714c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
17158d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank        try {
17165c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // Create our iCalendar writer and start generating tags
17175c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            SimpleIcsWriter ics = new SimpleIcsWriter();
17188d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("BEGIN", "VCALENDAR");
17198d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("METHOD", method);
17208d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("PRODID", "AndroidEmail");
17218d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("VERSION", "2.0");
1722820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
1723820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Our default vcalendar time zone is UTC, but this will change (below) if we're
1724820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // sending a recurring event, in which case we use local time
1725820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            TimeZone vCalendarTimeZone = sGmtTimeZone;
1726c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            String vCalendarDateSuffix = "";
1727c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank
1728c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            // Check for all day event
1729c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            boolean allDayEvent = false;
1730c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            if (entityValues.containsKey(Events.ALL_DAY)) {
1731c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
1732c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                allDayEvent = (ade != null) && (ade == 1);
1733c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                if (allDayEvent) {
1734c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                    // Example: DTSTART;VALUE=DATE:20100331 (all day event)
1735c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                    vCalendarDateSuffix = ";VALUE=DATE";
1736c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                }
1737c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            }
1738820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
1739820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // If we're inviting people and the meeting is recurring, we need to send our time zone
1740c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            // information and make sure to send DTSTART/DTEND in local time (unless, of course,
1741b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank            // this is an all-day event).  Recurring, for this purpose, includes exceptions to
1742b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank            // recurring events
1743b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank            if (!isReply && !allDayEvent &&
1744b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank                    (entityValues.containsKey(Events.RRULE) ||
17459e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik                            entityValues.containsKey(Events.ORIGINAL_SYNC_ID))) {
1746820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                vCalendarTimeZone = TimeZone.getDefault();
1747820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Write the VTIMEZONE block to the writer
1748820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                timeZoneToVTimezone(vCalendarTimeZone, ics);
1749c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                // Example: DTSTART;TZID=US/Pacific:20100331T124500
1750c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                vCalendarDateSuffix = ";TZID=" + vCalendarTimeZone.getID();
1751820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
1752820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
17538d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("BEGIN", "VEVENT");
17548d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (uid == null) {
175504c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik                uid = entityValues.getAsString(Events.SYNC_DATA2);
17568d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
17578d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (uid != null) {
17588d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                ics.writeTag("UID", uid);
17598d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
1760c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
17615c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            if (entityValues.containsKey("DTSTAMP")) {
17625c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                ics.writeTag("DTSTAMP", entityValues.getAsString("DTSTAMP"));
17635c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            } else {
176479268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                ics.writeTag("DTSTAMP", millisToEasDateTime(System.currentTimeMillis()));
17658d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
1766c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
17678d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            long startTime = entityValues.getAsLong(Events.DTSTART);
17685c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            if (startTime != 0) {
1769c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                ics.writeTag("DTSTART" + vCalendarDateSuffix,
1770c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                        millisToEasDateTime(startTime, vCalendarTimeZone, !allDayEvent));
17715c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            }
1772c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1773dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            // If this is an Exception, we send the recurrence-id, which is just the original
1774dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            // instance time
1775dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            if (isException) {
1776dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
1777c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                ics.writeTag("RECURRENCE-ID" + vCalendarDateSuffix,
1778c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                        millisToEasDateTime(originalTime, vCalendarTimeZone, !allDayEvent));
1779dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            }
1780dafc866120dac68fabbddcc9943e3995894c5244Marc Blank
17818d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (!entityValues.containsKey(Events.DURATION)) {
17828d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                if (entityValues.containsKey(Events.DTEND)) {
1783c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                    ics.writeTag("DTEND" + vCalendarDateSuffix,
178479268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                            millisToEasDateTime(
1785c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                                    entityValues.getAsLong(Events.DTEND), vCalendarTimeZone,
1786c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                                    !allDayEvent));
17878d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
17888d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            } else {
17898d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // Convert this into millis and add it to DTSTART for DTEND
17908d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // We'll use 1 hour as a default
17918d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                long durationMillis = HOURS;
17928d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                Duration duration = new Duration();
17938d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                try {
17948d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    duration.parse(entityValues.getAsString(Events.DURATION));
179589bee1e3d03b439f4084bc9962bb3cbffd0b878aMarc Blank                    durationMillis = duration.getMillis();
1796e6c2456aa6c00ef78c6d1d1621511d7ef8507f83Marc Blank                } catch (DateException e) {
17978d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    // We'll use the default in this case
17988d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
1799c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                ics.writeTag("DTEND" + vCalendarDateSuffix,
180079268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                        millisToEasDateTime(
1801c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                                startTime + durationMillis, vCalendarTimeZone, !allDayEvent));
1802c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1803c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1804dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            String location = null;
18058d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (entityValues.containsKey(Events.EVENT_LOCATION)) {
1806dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                location = entityValues.getAsString(Events.EVENT_LOCATION);
1807dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                ics.writeTag("LOCATION", location);
1808dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            }
1809dafc866120dac68fabbddcc9943e3995894c5244Marc Blank
181004c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik            String sequence = entityValues.getAsString(SYNC_VERSION);
1811dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            if (sequence == null) {
1812dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                sequence = "0";
18138d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
18145c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
181546e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            // We'll use 0 to mean a meeting invitation
18165c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            int titleId = 0;
18175c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            switch (messageFlag) {
18185c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_INVITE:
181946e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank                    if (!sequence.equals("0")) {
1820dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                        titleId = R.string.meeting_updated;
1821dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                    }
18225c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
18235c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_ACCEPT:
18245c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    titleId = R.string.meeting_accepted;
18255c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
18265c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_DECLINE:
18275c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    titleId = R.string.meeting_declined;
18285c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
18295c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
18305c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    titleId = R.string.meeting_tentative;
18315c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
183230d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank                case Message.FLAG_OUTGOING_MEETING_CANCEL:
183330d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank                    titleId = R.string.meeting_canceled;
183430d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank                    break;
18355c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            }
1836dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            Resources resources = context.getResources();
18378d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            String title = entityValues.getAsString(Events.TITLE);
18385c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            if (title == null) {
18395c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                title = "";
18408d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
18415c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            ics.writeTag("SUMMARY", title);
184246e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            // For meeting invitations just use the title
184346e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            if (titleId == 0) {
184446e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank                msg.mSubject = title;
184546e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            } else {
184646e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank                // Otherwise, use the additional text
1847dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                msg.mSubject = resources.getString(titleId, title);
184830d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank            }
184900702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank
185000702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            // Build the text for the message, starting with an initial line describing the
185100702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            // exception (if this is one)
185200702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            StringBuilder sb = new StringBuilder();
1853f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            if (isException && !isReply) {
185400702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                // Add the line, depending on whether this is a cancellation or update
185500702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                Date date = new Date(entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
185600702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                String dateString = DateFormat.getDateInstance().format(date);
185700702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                if (titleId == R.string.meeting_canceled) {
185800702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                    sb.append(resources.getString(R.string.exception_cancel, dateString));
185900702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                } else {
186000702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                    sb.append(resources.getString(R.string.exception_updated, dateString));
186100702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                }
186200702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                sb.append("\n\n");
186300702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            }
186400702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            String text =
186500702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                CalendarUtilities.buildMessageTextFromEntityValues(context, entityValues, sb);
186600702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank
186700702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            if (text.length() > 0) {
186800702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                ics.writeTag("DESCRIPTION", text);
186900702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            }
187000702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            // And store the message text
187100702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            msg.mText = text;
1872f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            if (!isReply) {
18735c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                if (entityValues.containsKey(Events.ALL_DAY)) {
18745c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
18755c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    ics.writeTag("X-MICROSOFT-CDO-ALLDAYEVENT", ade == 0 ? "FALSE" : "TRUE");
18765c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                }
1877c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
18785c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                String rrule = entityValues.getAsString(Events.RRULE);
18795c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                if (rrule != null) {
18805c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    ics.writeTag("RRULE", rrule);
18815c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                }
18825c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
188300702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                // If we decide to send alarm information in the meeting request ics file,
188400702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                // handle it here by looping through the subvalues
1885c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1886c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1887332d08cc2fc37d9936a73e3a120125c60b0587d1Marc Blank            // Handle attendee data here; determine "to" list and add ATTENDEE tags to ics
18888d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            String organizerName = null;
18898d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            String organizerEmail = null;
18908d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ArrayList<Address> toList = new ArrayList<Address>();
18918d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            for (NamedContentValues ncv: subValues) {
18928d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                Uri ncvUri = ncv.uri;
18938d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                ContentValues ncvValues = ncv.values;
18948d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                if (ncvUri.equals(Attendees.CONTENT_URI)) {
18958d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    Integer relationship =
18968d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
18978d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    // If there's no relationship, we can't create this for EAS
18988d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    // Similarly, we need an attendee email for each invitee
18998d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    if (relationship != null &&
19008d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                            ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
19018d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        // Organizer isn't among attendees in EAS
19028d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
19038d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                            organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
19048d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                            organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
19058d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                            continue;
1906c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                        }
19078d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        String attendeeEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
19088d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
190924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
19108d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        // This shouldn't be possible, but allow for it
19118d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        if (attendeeEmail == null) continue;
191224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        // If we only want to send to the specifiedAttendee, eliminate others here
191324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        if ((specifiedAttendee != null) &&
191424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                                !attendeeEmail.equalsIgnoreCase(specifiedAttendee)) {
191524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                            continue;
1916c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                        }
191724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
191824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        addAttendeeToMessage(ics, toList, attendeeName, attendeeEmail, messageFlag,
191924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                                account);
1920c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                    }
1921c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                }
1922c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1923c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
192424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            // Manually add the specifiedAttendee if he wasn't added in the Attendees loop
192524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (toList.isEmpty() && (specifiedAttendee != null)) {
192624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                addAttendeeToMessage(ics, toList, null, specifiedAttendee, messageFlag, account);
192724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
192824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
19298d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            // Create the organizer tag for ical
19308d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (organizerEmail != null) {
19318d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                String icalTag = "ORGANIZER";
19328d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // We should be able to find this, assuming the Email is the user's email
19338d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // TODO Find this in the account
19348d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                if (organizerName != null) {
1935bc0c8c1523fc0bc42eb82dba6c2977492e441c03Makoto Onuki                    icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(organizerName);
19368d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
19378d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                ics.writeTag(icalTag, "MAILTO:" + organizerEmail);
1938f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank                if (isReply) {
19398d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    toList.add(organizerName == null ? new Address(organizerEmail) :
19408d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        new Address(organizerEmail, organizerName));
19418d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
1942c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1943c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
194424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            // If we have no "to" list, we're done
194524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (toList.isEmpty()) return null;
1946601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank
19478d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            // Write out the "to" list
19488d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            Address[] toArray = new Address[toList.size()];
19498d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            int i = 0;
19508d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            for (Address address: toList) {
19518d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                toArray[i++] = address;
19528d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
19538d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            msg.mTo = Address.pack(toArray);
19548d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank
1955820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            ics.writeTag("CLASS", "PUBLIC");
1956dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            ics.writeTag("STATUS", (messageFlag == Message.FLAG_OUTGOING_MEETING_CANCEL) ?
1957dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                    "CANCELLED" : "CONFIRMED");
1958820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            ics.writeTag("TRANSP", "OPAQUE"); // What Exchange uses
1959820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            ics.writeTag("PRIORITY", "5");  // 1 to 9, 5 = medium
1960dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            ics.writeTag("SEQUENCE", sequence);
19618d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("END", "VEVENT");
19628d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("END", "VCALENDAR");
19635c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
19645c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // Create the ics attachment using the "content" field
19655c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            Attachment att = new Attachment();
1966bc0c8c1523fc0bc42eb82dba6c2977492e441c03Makoto Onuki            att.mContentBytes = ics.getBytes();
19675c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            att.mMimeType = "text/calendar; method=" + method;
19685c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            att.mFileName = "invite.ics";
1969e54b75dc4f638e594e9b97e3b4ed8829fbc9b521Makoto Onuki            att.mSize = att.mContentBytes.length;
19705c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // We don't send content-disposition with this attachment
1971b4d217e5170ae397d741e95308d98e80d0c2f637Marc Blank            att.mFlags = Attachment.FLAG_ICS_ALTERNATIVE_PART;
19725c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
19735c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // Add the attachment to the message
19745c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            msg.mAttachments = new ArrayList<Attachment>();
19755c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            msg.mAttachments.add(att);
19765c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        } catch (IOException e) {
19775c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            Log.w(TAG, "IOException in createMessageForEntity");
19788d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            return null;
1979c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        }
1980c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1981c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        // Return the new Message to caller
1982c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        return msg;
1983c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    }
1984c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1985c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    /**
19866989716b639d274a98141674556ac9402be32ebeRoboErik     * Create a Message for an Event that can be retrieved from CalendarProvider
19876989716b639d274a98141674556ac9402be32ebeRoboErik     * by its unique id
19886989716b639d274a98141674556ac9402be32ebeRoboErik     *
1989c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param cr a content resolver that can be used to query for the Event
1990c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param eventId the unique id of the Event
19916989716b639d274a98141674556ac9402be32ebeRoboErik     * @param messageFlag the Message.FLAG_XXX constant indicating the type of
19926989716b639d274a98141674556ac9402be32ebeRoboErik     *            email to be sent
19936989716b639d274a98141674556ac9402be32ebeRoboErik     * @param the unique id of this Event, or null if it can be retrieved from
19946989716b639d274a98141674556ac9402be32ebeRoboErik     *            the Event
1995c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param the user's account
19966989716b639d274a98141674556ac9402be32ebeRoboErik     * @param requireAddressees if true (the default), no Message is returned if
19976989716b639d274a98141674556ac9402be32ebeRoboErik     *            there aren't any addressees; if false, return the Message
19986989716b639d274a98141674556ac9402be32ebeRoboErik     *            regardless (addressees will be filled in later)
1999c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @return a Message with many fields pre-filled (more later)
20006989716b639d274a98141674556ac9402be32ebeRoboErik     * @throws RemoteException if there is an issue retrieving the Event from
20016989716b639d274a98141674556ac9402be32ebeRoboErik     *             CalendarProvider
2002c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     */
20035c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank    static public EmailContent.Message createMessageForEventId(Context context, long eventId,
2004c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            int messageFlag, String uid, Account account) throws RemoteException {
2005601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank        return createMessageForEventId(context, eventId, messageFlag, uid, account,
20066989716b639d274a98141674556ac9402be32ebeRoboErik                null /* specifiedAttendee */);
2007601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    }
2008601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank
2009601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    static public EmailContent.Message createMessageForEventId(Context context, long eventId,
201024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            int messageFlag, String uid, Account account, String specifiedAttendee)
2011601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank            throws RemoteException {
20125c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        ContentResolver cr = context.getContentResolver();
20136989716b639d274a98141674556ac9402be32ebeRoboErik        EntityIterator eventIterator = EventsEntity.newEntityIterator(cr.query(
20146989716b639d274a98141674556ac9402be32ebeRoboErik                ContentUris.withAppendedId(Events.CONTENT_URI, eventId), null, null, null, null),
20156989716b639d274a98141674556ac9402be32ebeRoboErik                cr);
2016c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        try {
2017c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            while (eventIterator.hasNext()) {
2018c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                Entity entity = eventIterator.next();
2019601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank                return createMessageForEntity(context, entity, messageFlag, uid, account,
202024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        specifiedAttendee);
2021c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
2022c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        } finally {
2023c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            eventIterator.close();
2024c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        }
2025c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        return null;
2026c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    }
2027393208ab154d18a876842777781ab153d34a0281Marc Blank
2028393208ab154d18a876842777781ab153d34a0281Marc Blank    /**
2029393208ab154d18a876842777781ab153d34a0281Marc Blank     * Return a boolean value for an integer ContentValues column
2030393208ab154d18a876842777781ab153d34a0281Marc Blank     * @param values a ContentValues object
2031393208ab154d18a876842777781ab153d34a0281Marc Blank     * @param columnName the name of a column to be found in the ContentValues
2032393208ab154d18a876842777781ab153d34a0281Marc Blank     * @return a boolean representation of the value of columnName in values; null and 0 = false,
2033393208ab154d18a876842777781ab153d34a0281Marc Blank     * other integers = true
2034393208ab154d18a876842777781ab153d34a0281Marc Blank     */
2035393208ab154d18a876842777781ab153d34a0281Marc Blank    static public boolean getIntegerValueAsBoolean(ContentValues values, String columnName) {
2036393208ab154d18a876842777781ab153d34a0281Marc Blank        Integer intValue = values.getAsInteger(columnName);
2037393208ab154d18a876842777781ab153d34a0281Marc Blank        return (intValue != null && intValue != 0);
2038393208ab154d18a876842777781ab153d34a0281Marc Blank    }
20398e26c42accbaf72eff6694173496aba0e6aa37f6Mihai Preda}
2040