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
38f352bc9f29cacc61b195069e48d5c8b868660694Marc Blankimport com.android.emailcommon.Logging;
3960df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.mail.Address;
4060df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.Account;
4160df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.EmailContent;
4260df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.EmailContent.Attachment;
4360df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.EmailContent.Message;
4460df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.provider.Mailbox;
4560df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.service.AccountServiceProxy;
4660df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.emailcommon.utility.Utility;
4760df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.Eas;
4860df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.EasSyncService;
4960df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.ExchangeService;
5060df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.R;
5160df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.adapter.Serializer;
5260df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.exchange.adapter.Tags;
5360df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.android.internal.util.ArrayUtils;
5460df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blankimport com.google.common.annotations.VisibleForTesting;
5560df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank
5614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blankimport java.io.IOException;
57dafc866120dac68fabbddcc9943e3995894c5244Marc Blankimport java.text.DateFormat;
58c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blankimport java.text.ParseException;
59c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blankimport java.util.ArrayList;
605862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.Calendar;
615862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.Date;
625862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.GregorianCalendar;
635862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.HashMap;
645862a85e17e81866ca82a9905577931947fbd44eMarc Blankimport java.util.TimeZone;
655862a85e17e81866ca82a9905577931947fbd44eMarc Blank
665862a85e17e81866ca82a9905577931947fbd44eMarc Blankpublic class CalendarUtilities {
6704c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik
685862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // NOTE: Most definitions in this class are have package visibility for testing purposes
695862a85e17e81866ca82a9905577931947fbd44eMarc Blank    private static final String TAG = "CalendarUtility";
705862a85e17e81866ca82a9905577931947fbd44eMarc Blank
715862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Time related convenience constants, in milliseconds
725862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int SECONDS = 1000;
735862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MINUTES = SECONDS*60;
745862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int HOURS = MINUTES*60;
75377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static final long DAYS = HOURS*24;
765862a85e17e81866ca82a9905577931947fbd44eMarc Blank
772c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // We want to find a time zone whose DST info is accurate to one minute
782c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static final int STANDARD_DST_PRECISION = MINUTES;
792c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // If we can't find one, we'll try a more lenient standard (this is better than guessing a
802c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // time zone, which is what we otherwise do).  Note that this specifically addresses an issue
812c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // seen in some time zones sent by MS Exchange in which the start and end hour differ
822c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    // for no apparent reason
832c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static final int LENIENT_DST_PRECISION = 4*HOURS;
842c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank
8504c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik    private static final String SYNC_VERSION = Events.SYNC_DATA4;
865862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // NOTE All Microsoft data structures are little endian
875862a85e17e81866ca82a9905577931947fbd44eMarc Blank
885862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // The following constants relate to standard Microsoft data sizes
895862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx
905862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_LONG_SIZE = 4;
915862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_WCHAR_SIZE = 2;
925862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_WORD_SIZE = 2;
935862a85e17e81866ca82a9905577931947fbd44eMarc Blank
945862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // The following constants relate to Microsoft's SYSTEMTIME structure
955862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4
965862a85e17e81866ca82a9905577931947fbd44eMarc Blank
975862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE;
985862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE;
995862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE;
1005862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE;
1015862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE;
1025862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE;
1035862a85e17e81866ca82a9905577931947fbd44eMarc Blank    //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE;
1045862a85e17e81866ca82a9905577931947fbd44eMarc Blank    //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE;
1055862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE;
1065862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1075862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure
1085862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx
1095862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0;
1105862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET =
1115862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE;
1125862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET =
1135862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
1145862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET =
1155862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
1165862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET =
1175862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE;
1185862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET =
1195862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
1205862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET =
1215862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
1225862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final int MSFT_TIME_ZONE_SIZE =
1235862a85e17e81866ca82a9905577931947fbd44eMarc Blank        MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE;
1245862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1255862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // TimeZone cache; we parse/decode as little as possible, because the process is quite slow
1265862a85e17e81866ca82a9905577931947fbd44eMarc Blank    private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>();
127377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    // TZI string cache; we keep around our encoded TimeZoneInformation strings
128377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    private static HashMap<TimeZone, String> sTziStringCache = new HashMap<TimeZone, String>();
1295862a85e17e81866ca82a9905577931947fbd44eMarc Blank
130270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
131937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    // Default, Popup
13280a8e57ce2dc2695ed6f35599d326090e4ad9faeRoboErik    private static final String ALLOWED_REMINDER_TYPES = "0,1";
133937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    // None, required, optional
134937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    private static final String ALLOWED_ATTENDEE_TYPES = "0,1,2";
135937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    // Busy, free, tentative
136937af5abcbc1268f22a3058b00835c74ba20f116RoboErik    private static final String ALLOWED_AVAILABILITIES = "0,1,2";
137270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank
1385862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // There is no type 4 (thus, the "")
1395862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final String[] sTypeToFreq =
1405862a85e17e81866ca82a9905577931947fbd44eMarc Blank        new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"};
1415862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1425862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final String[] sDayTokens =
1435862a85e17e81866ca82a9905577931947fbd44eMarc Blank        new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
1445862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1455862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static final String[] sTwoCharacterNumbers =
1465862a85e17e81866ca82a9905577931947fbd44eMarc Blank        new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"};
1475862a85e17e81866ca82a9905577931947fbd44eMarc Blank
148f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    // Bits used in EAS recurrences for days of the week
149f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_SUNDAY = 1<<0;
150f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_MONDAY = 1<<1;
151f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_TUESDAY = 1<<2;
152f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_WEDNESDAY = 1<<3;
153f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_THURSDAY = 1<<4;
154f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_FRIDAY = 1<<5;
155f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_SATURDAY = 1<<6;
156f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_WEEKDAYS =
157f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        EAS_MONDAY | EAS_TUESDAY | EAS_WEDNESDAY | EAS_THURSDAY | EAS_FRIDAY;
158f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    protected static final int EAS_WEEKENDS = EAS_SATURDAY | EAS_SUNDAY;
159f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank
160377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR);
161377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT");
162377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
163bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    private static final String ICALENDAR_ATTENDEE = "ATTENDEE;ROLE=REQ-PARTICIPANT";
164bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_CANCEL = ICALENDAR_ATTENDEE;
165bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_INVITE =
166bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=NEEDS-ACTION;RSVP=TRUE";
167bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_ACCEPT =
168bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=ACCEPTED";
169bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_DECLINE =
170bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=DECLINED";
171bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank    static final String ICALENDAR_ATTENDEE_TENTATIVE =
172bf1de871b7ec63c93694ba022282e8789e69f201Marc Blank        ICALENDAR_ATTENDEE + ";PARTSTAT=TENTATIVE";
173c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
174e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // Note that these constants apply to Calendar items
175e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // For future reference: MeetingRequest data can also include free/busy information, but the
176e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // constants for these four options in MeetingRequest data have different values!
177e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // See [MS-ASCAL] 2.2.2.8 for Calendar BusyStatus
178e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    // See [MS-EMAIL] 2.2.2.34 for MeetingRequest BusyStatus
179e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_FREE = 0;
180e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_TENTATIVE = 1;
181e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_BUSY = 2;
182e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank    public static final int BUSY_STATUS_OUT_OF_OFFICE = 3;
183dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank
18465d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    // Note that these constants apply to Calendar items, and are used in EAS 14+
18565d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    // See [MS-ASCAL] 2.2.2.22 for Calendar ResponseType
18665d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_NONE = 0;
18765d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_ORGANIZER = 1;
18865d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_TENTATIVE = 2;
18965d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_ACCEPTED = 3;
19065d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_DECLINED = 4;
19165d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    public static final int RESPONSE_TYPE_NOT_RESPONDED = 5;
19265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank
1935862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Return a 4-byte long from a byte array (little endian)
1945862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static int getLong(byte[] bytes, int offset) {
1955862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) |
1965862a85e17e81866ca82a9905577931947fbd44eMarc Blank        ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24);
1975862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
1985862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1995862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Put a 4-byte long into a byte array (little endian)
2005862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static void setLong(byte[] bytes, int offset, int value) {
2015862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) (value & 0xFF);
2025862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) ((value >> 8) & 0xFF);
2035862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) ((value >> 16) & 0xFF);
2045862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset] = (byte) ((value >> 24) & 0xFF);
2055862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2065862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2075862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Return a 2-byte word from a byte array (little endian)
2085862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static int getWord(byte[] bytes, int offset) {
2095862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8);
2105862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2115862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2125862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Put a 2-byte word into a byte array (little endian)
2135862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static void setWord(byte[] bytes, int offset, int value) {
2145862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset++] = (byte) (value & 0xFF);
2155862a85e17e81866ca82a9905577931947fbd44eMarc Blank        bytes[offset] = (byte) ((value >> 8) & 0xFF);
2165862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2175862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2185862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Internal structure for storing a time zone date from a SYSTEMTIME structure
2195862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // This date represents either the start or the end time for DST
2205862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static class TimeZoneDate {
2215862a85e17e81866ca82a9905577931947fbd44eMarc Blank        String year;
2225862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int month;
2235862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int dayOfWeek;
2245862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int day;
2255862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int time;
2265862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int hour;
2275862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int minute;
2285862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
2295862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2304868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank    @VisibleForTesting
2314868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank    static void clearTimeZoneCache() {
2324868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank        sTimeZoneCache.clear();
2334868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank    }
2344868e0f09b58104741a5593f6097589ac62c6ce3Marc Blank
235820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static void putRuleIntoTimeZoneInformation(byte[] bytes, int offset, RRule rrule, int hour,
236820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            int minute) {
237820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // MSFT months are 1 based, same as RRule
238820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, rrule.month);
239820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // MSFT day of week starts w/ Sunday = 0; RRule starts w/ Sunday = 1
240820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, rrule.dayOfWeek - 1);
241820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // 5 means "last" in MSFT land; for RRule, it's -1
242820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, rrule.week < 0 ? 5 : rrule.week);
243820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Turn hours/minutes into ms from midnight (per TimeZone)
244820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, hour);
245820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, minute);
246820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
247820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
24810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    // Write a transition time into SYSTEMTIME data (via an offset into a byte array)
24910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    static void putTransitionMillisIntoSystemTime(byte[] bytes, int offset, long millis) {
250377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
251377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // Round to the next highest minute; we always write seconds as zero
252377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        cal.setTimeInMillis(millis + 30*SECONDS);
253377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
254377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // MSFT months are 1 based; TimeZone is 0 based
255377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, cal.get(Calendar.MONTH) + 1);
256377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
257377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, cal.get(Calendar.DAY_OF_WEEK) - 1);
258377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
259377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // Get the "day" in TimeZone format
260377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        int wom = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
261377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // 5 means "last" in MSFT land; for TimeZone, it's -1
262377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, wom < 0 ? 5 : wom);
263377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
264377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        // Turn hours/minutes into ms from midnight (per TimeZone)
26510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, getTrueTransitionHour(cal));
26610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, getTrueTransitionMinute(cal));
267377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     }
268377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
2695862a85e17e81866ca82a9905577931947fbd44eMarc Blank    // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset
2705862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) {
2715862a85e17e81866ca82a9905577931947fbd44eMarc Blank        TimeZoneDate tzd = new TimeZoneDate();
2725862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2735862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT year is an int; TimeZone is a String
2745862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR);
2755862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.year = Integer.toString(num);
2765862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2775862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT month = 0 means no daylight time
2785862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT months are 1 based; TimeZone is 0 based
2795862a85e17e81866ca82a9905577931947fbd44eMarc Blank        num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH);
2805862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (num == 0) {
2815862a85e17e81866ca82a9905577931947fbd44eMarc Blank            return null;
2825862a85e17e81866ca82a9905577931947fbd44eMarc Blank        } else {
2835862a85e17e81866ca82a9905577931947fbd44eMarc Blank            tzd.month = num -1;
2845862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
2855862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2865862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
2875862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1;
2885862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2895862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Get the "day" in TimeZone format
2905862a85e17e81866ca82a9905577931947fbd44eMarc Blank        num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY);
2915862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // 5 means "last" in MSFT land; for TimeZone, it's -1
2925862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (num == 5) {
2935862a85e17e81866ca82a9905577931947fbd44eMarc Blank            tzd.day = -1;
2945862a85e17e81866ca82a9905577931947fbd44eMarc Blank        } else {
2955862a85e17e81866ca82a9905577931947fbd44eMarc Blank            tzd.day = num;
2965862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
2975862a85e17e81866ca82a9905577931947fbd44eMarc Blank
2985862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Turn hours/minutes into ms from midnight (per TimeZone)
2995862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR);
3005862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.hour = hour;
3015862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE);
3025862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.minute = minute;
3035862a85e17e81866ca82a9905577931947fbd44eMarc Blank        tzd.time = (hour*HOURS) + (minute*MINUTES);
3045862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3055862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return tzd;
3065862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
3075862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3085862a85e17e81866ca82a9905577931947fbd44eMarc Blank    /**
3095862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * Build a GregorianCalendar, based on a time zone and TimeZoneDate.
3105862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @param timeZone the time zone we're checking
3115862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @param tzd the TimeZoneDate we're interested in
3125862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @return a GregorianCalendar with the given time zone and date
3135862a85e17e81866ca82a9905577931947fbd44eMarc Blank     */
31479268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static long getMillisAtTimeZoneDateTransition(TimeZone timeZone, TimeZoneDate tzd) {
3155862a85e17e81866ca82a9905577931947fbd44eMarc Blank        GregorianCalendar testCalendar = new GregorianCalendar(timeZone);
316377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        testCalendar.set(GregorianCalendar.YEAR, sCurrentYear);
3175862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.MONTH, tzd.month);
3185862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek);
3195862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day);
3205862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour);
3215862a85e17e81866ca82a9905577931947fbd44eMarc Blank        testCalendar.set(GregorianCalendar.MINUTE, tzd.minute);
32279268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        testCalendar.set(GregorianCalendar.SECOND, 0);
32379268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        return testCalendar.getTimeInMillis();
3245862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
3255862a85e17e81866ca82a9905577931947fbd44eMarc Blank
3265862a85e17e81866ca82a9905577931947fbd44eMarc Blank    /**
327820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Return a GregorianCalendar representing the first standard/daylight transition between a
328820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * start time and an end time in the given time zone
329820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz a TimeZone the time zone in which we're looking for transitions
330377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param startTime the start time for the test
331377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param endTime the end time for the test
332377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param startInDaylightTime whether daylight time is in effect at the startTime
333820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return a GregorianCalendar representing the transition or null if none
334377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
33579268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static GregorianCalendar findTransitionDate(TimeZone tz, long startTime,
33610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            long endTime, boolean startInDaylightTime) {
337377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        long startingEndTime = endTime;
338377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        Date date = null;
33910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank
340820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll keep splitting the difference until we're within a minute
341377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        while ((endTime - startTime) > MINUTES) {
342377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            long checkTime = ((startTime + endTime) / 2) + 1;
343377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            date = new Date(checkTime);
34410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            boolean inDaylightTime = tz.inDaylightTime(date);
34510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            if (inDaylightTime != startInDaylightTime) {
346377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                endTime = checkTime;
347377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            } else {
348377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                startTime = checkTime;
349377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
350377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
351820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
352820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // If these are the same, we're really messed up; return null
353377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (endTime == startingEndTime) {
354820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return null;
355377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
356820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
35710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        // Set up our calendar and return it
358820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar calendar = new GregorianCalendar(tz);
35910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        calendar.setTimeInMillis(startTime);
360820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return calendar;
361377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
362377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
363377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
364377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Return a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
365377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * that might be found in an Event; use cached result, if possible
366377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param tz the TimeZone
367377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
368377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
369377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static public String timeZoneToTziString(TimeZone tz) {
370377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        String tziString = sTziStringCache.get(tz);
371377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (tziString != null) {
372377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            if (Eas.USER_LOG) {
373385a0be662509754e687bcfa9813208b050bf951Marc Blank                ExchangeService.log(TAG, "TZI string for " + tz.getDisplayName() +
374385a0be662509754e687bcfa9813208b050bf951Marc Blank                        " found in cache.");
375377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
376377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            return tziString;
377377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
378377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        tziString = timeZoneToTziStringImpl(tz);
379377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sTziStringCache.put(tz, tziString);
380377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return tziString;
381377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
382377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
383377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
384820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * A class for storing RRULE information.  The RRULE members can be accessed individually or
385820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * an RRULE string can be created with toString()
386820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
387820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static class RRule {
388820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        static final int RRULE_NONE = 0;
389820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        static final int RRULE_DAY_WEEK = 1;
390820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        static final int RRULE_DATE = 2;
391820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
392820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int type;
393820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int dayOfWeek;
394820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int week;
395820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int month;
396820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int date;
397820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
398820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        /**
399820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * Create an RRULE based on month and date
400820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _month the month (1 = JAN, 12 = DEC)
401820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _date the date in the month (1-31)
402820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         */
403820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule(int _month, int _date) {
404820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            type = RRULE_DATE;
405820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            month = _month;
406820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            date = _date;
407820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
408820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
409820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        /**
410820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * Create an RRULE based on month, day of week, and week #
411820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _month the month (1 = JAN, 12 = DEC)
412820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _dayOfWeek the day of the week (1 = SU, 7 = SA)
413820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         * @param _week the week in the month (1-5 or -1 for last)
414820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank         */
415820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule(int _month, int _dayOfWeek, int _week) {
416820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            type = RRULE_DAY_WEEK;
417820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            month = _month;
418820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            dayOfWeek = _dayOfWeek;
419820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            week = _week;
420820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
421820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
422820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        @Override
423820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        public String toString() {
424820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (type == RRULE_DAY_WEEK) {
425820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return "FREQ=YEARLY;BYMONTH=" + month + ";BYDAY=" + week +
426820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    sDayTokens[dayOfWeek - 1];
427820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
428820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return "FREQ=YEARLY;BYMONTH=" + month + ";BYMONTHDAY=" + date;
429820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
430820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank       }
431820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
432820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
433820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
434820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Generate an RRULE string for an array of GregorianCalendars, if possible.  For now, we are
435820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * only looking for rules based on the same date in a month or a specific instance of a day of
436820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the week in a month (e.g. 2nd Tuesday or last Friday).  Indeed, these are the only kinds of
437820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * rules used in the current tzinfo database.
438820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param calendars an array of GregorianCalendar, set to a series of transition times in
439820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * consecutive years starting with the current year
440820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return an RRULE or null if none could be inferred from the calendars
441820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
44279268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static RRule inferRRuleFromCalendars(GregorianCalendar[] calendars) {
443820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Let's see if we can make a rule about these
444820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar calendar = calendars[0];
445820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (calendar == null) return null;
446820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int month = calendar.get(Calendar.MONTH);
447820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int date = calendar.get(Calendar.DAY_OF_MONTH);
448820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
449820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int week = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
450820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int maxWeek = calendar.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
451820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        boolean dateRule = false;
452820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        boolean dayOfWeekRule = false;
453820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        for (int i = 1; i < calendars.length; i++) {
454820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar cal = calendars[i];
455820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal == null) return null;
456820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // If it's not the same month, there's no rule
457820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal.get(Calendar.MONTH) != month) {
458820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return null;
459820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (dayOfWeek == cal.get(Calendar.DAY_OF_WEEK)) {
460820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Ok, it seems to be the same day of the week
461820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (dateRule) {
462820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    return null;
463820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
464820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                dayOfWeekRule = true;
465820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                int thisWeek = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
466820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (week != thisWeek) {
467820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    if (week < 0 || week == maxWeek) {
468820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        int thisMaxWeek = cal.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
469820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        if (thisWeek == thisMaxWeek) {
470820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            // We'll use -1 (i.e. last) week
471820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            week = -1;
472820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            continue;
473820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        }
474820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    }
475820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    return null;
476820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
477820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (date == cal.get(Calendar.DAY_OF_MONTH)) {
478820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Maybe the same day of the month?
479820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (dayOfWeekRule) {
480820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    return null;
481820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
482820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                dateRule = true;
483820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
484820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return null;
485820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
486820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
487820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
488820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (dateRule) {
489820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return new RRule(month + 1, date);
490820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
491820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // sDayTokens is 0 based (SU = 0); Calendar days of week are 1 based (SU = 1)
492820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // iCalendar months are 1 based; Calendar months are 0 based
493820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // So we adjust these when building the string
494820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return new RRule(month + 1, dayOfWeek, week);
495820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
496820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
497820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
498820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Generate an rfc2445 utcOffset from minutes offset from GMT
499820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * These look like +0800 or -0100
500820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param offsetMinutes minutes offset from GMT (east is positive, west is negative
501820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return a utcOffset
502820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
50379268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static String utcOffsetString(int offsetMinutes) {
504820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        StringBuilder sb = new StringBuilder();
505820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int hours = offsetMinutes / 60;
506820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hours < 0) {
507820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('-');
508820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            hours = 0 - hours;
509820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        } else {
510820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('+');
511820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
512820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int minutes = offsetMinutes % 60;
513820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hours < 10) {
514820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('0');
515820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
516820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(hours);
517820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (minutes < 10) {
518820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            sb.append('0');
519820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
520820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(minutes);
521820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return sb.toString();
522820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
523820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
524820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
525820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Fill the passed in GregorianCalendars arrays with DST transition information for this and
526820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the following years (based on the length of the arrays)
527820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz the time zone
528820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param toDaylightCalendars an array of GregorianCalendars, one for each year, representing
529820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the transition to daylight time
530820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param toStandardCalendars an array of GregorianCalendars, one for each year, representing
531820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * the transition to standard time
532820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return true if transitions could be found for all years, false otherwise
533820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
534820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static boolean getDSTCalendars(TimeZone tz, GregorianCalendar[] toDaylightCalendars,
535820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar[] toStandardCalendars) {
536820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll use the length of the arrays to determine how many years to check
537820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int maxYears = toDaylightCalendars.length;
538820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (toStandardCalendars.length != maxYears) {
539820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return false;
540820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
541820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Get the transitions for this year and the next few years
542820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        for (int i = 0; i < maxYears; i++) {
543820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar cal = new GregorianCalendar(tz);
544820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            cal.set(sCurrentYear + i, Calendar.JANUARY, 1, 0, 0, 0);
545820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            long startTime = cal.getTimeInMillis();
546820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Calculate end of year; no need to be insanely precise
547820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            long endOfYearTime = startTime + (365*DAYS) + (DAYS>>2);
548820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            Date date = new Date(startTime);
549820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            boolean startInDaylightTime = tz.inDaylightTime(date);
550820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Find the first transition, and store
551820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            cal = findTransitionDate(tz, startTime, endOfYearTime, startInDaylightTime);
552820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal == null) {
553820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return false;
554820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (startInDaylightTime) {
555820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toStandardCalendars[i] = cal;
556820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
557820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toDaylightCalendars[i] = cal;
558820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
559820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Find the second transition, and store
560820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            cal = findTransitionDate(tz, startTime, endOfYearTime, !startInDaylightTime);
561820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (cal == null) {
562820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return false;
563820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else if (startInDaylightTime) {
564820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toDaylightCalendars[i] = cal;
565820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            } else {
566820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                toStandardCalendars[i] = cal;
567820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
568820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
569820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return true;
570820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
571820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
572820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
573820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Write out the STANDARD block of VTIMEZONE and end the VTIMEZONE
574820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param writer the SimpleIcsWriter we're using
575820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz the time zone
576820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param offsetString the offset string in VTIMEZONE format (e.g. +0800)
577820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @throws IOException
578820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
579820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    static private void writeNoDST(SimpleIcsWriter writer, TimeZone tz, String offsetString)
580820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            throws IOException {
581820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "STANDARD");
582820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETFROM", offsetString);
583820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETTO", offsetString);
584820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Might as well use start of epoch for start date
585820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("DTSTART", millisToEasDateTime(0L));
586820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "STANDARD");
587820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "VTIMEZONE");
588820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
589820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
590820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /** Write a VTIMEZONE block for a given TimeZone into a SimpleIcsWriter
591820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz the TimeZone to be used in the conversion
592820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param writer the SimpleIcsWriter to be used
593820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @throws IOException
594820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
59579268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static void timeZoneToVTimezone(TimeZone tz, SimpleIcsWriter writer)
596820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            throws IOException {
597820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll use these regardless of whether there's DST in this time zone or not
598820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int rawOffsetMinutes = tz.getRawOffset() / MINUTES;
599820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        String standardOffsetString = utcOffsetString(rawOffsetMinutes);
600820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
601820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Preamble for all of our VTIMEZONEs
602820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "VTIMEZONE");
603820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZID", tz.getID());
604820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("X-LIC-LOCATION", tz.getDisplayName());
605820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
606820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Simplest case is no daylight time
607820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (!tz.useDaylightTime()) {
608820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writeNoDST(writer, tz, standardOffsetString);
609820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return;
610820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
611820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
612820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        int maxYears = 3;
613820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[maxYears];
614820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar[] toStandardCalendars = new GregorianCalendar[maxYears];
615820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (!getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) {
616820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writeNoDST(writer, tz, standardOffsetString);
617820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            return;
618820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
619820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Try to find a rule to cover these yeras
620820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars);
621820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        RRule standardRule = inferRRuleFromCalendars(toStandardCalendars);
622820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        String daylightOffsetString =
623820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            utcOffsetString(rawOffsetMinutes + (tz.getDSTSavings() / MINUTES));
624820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // We'll use RRULE's if we found both
625820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Otherwise we write the first as DTSTART and the others as RDATE
626820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        boolean hasRule = daylightRule != null && standardRule != null;
627820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
628820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Write the DAYLIGHT block
629820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "DAYLIGHT");
630820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETFROM", standardOffsetString);
631820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETTO", daylightOffsetString);
632820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("DTSTART",
63310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                transitionMillisToVCalendarTime(
63410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        toDaylightCalendars[0].getTimeInMillis(), tz, true));
635820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hasRule) {
636820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writer.writeTag("RRULE", daylightRule.toString());
637820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        } else {
638820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            for (int i = 1; i < maxYears; i++) {
63910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                writer.writeTag("RDATE", transitionMillisToVCalendarTime(
640820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        toDaylightCalendars[i].getTimeInMillis(), tz, true));
641820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
642820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
643820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "DAYLIGHT");
644820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // Write the STANDARD block
645820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("BEGIN", "STANDARD");
646820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETFROM", daylightOffsetString);
647820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("TZOFFSETTO", standardOffsetString);
648820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("DTSTART",
64910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                transitionMillisToVCalendarTime(
65010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        toStandardCalendars[0].getTimeInMillis(), tz, false));
651820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        if (hasRule) {
652820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            writer.writeTag("RRULE", standardRule.toString());
653820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        } else {
654820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            for (int i = 1; i < maxYears; i++) {
65510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                writer.writeTag("RDATE", transitionMillisToVCalendarTime(
656820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        toStandardCalendars[i].getTimeInMillis(), tz, true));
657820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
658820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
659820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "STANDARD");
660820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // And we're done
661820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        writer.writeTag("END", "VTIMEZONE");
662820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
663820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
664820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
665820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * Find the next transition to occur (i.e. after the current date/time)
666820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param transitions calendars representing transitions to/from DST
667820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @return millis for the first transition after the current date/time
668820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
66979268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static long findNextTransition(long startingMillis, GregorianCalendar[] transitions) {
670820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        for (GregorianCalendar transition: transitions) {
671820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            long transitionMillis = transition.getTimeInMillis();
672820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (transitionMillis > startingMillis) {
673820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                return transitionMillis;
674820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
675820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
676820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return 0;
677820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
678820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
679820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
680377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Calculate the Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
681377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * that might be found in an Event.  Since the internal representation of the TimeZone is hidden
682377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * from us we'll find the DST transitions and build the structure from that information
683377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param tz the TimeZone
684377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
685377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
68679268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static String timeZoneToTziStringImpl(TimeZone tz) {
687377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        String tziString;
688377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE];
689377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        int standardBias = - tz.getRawOffset();
690377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        standardBias /= 60*SECONDS;
691377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias);
692820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        // If this time zone has daylight savings time, we need to do more work
693377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (tz.useDaylightTime()) {
694820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[3];
695820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            GregorianCalendar[] toStandardCalendars = new GregorianCalendar[3];
696820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // See if we can get transitions for a few years; if not, we can't generate DST info
697820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // for this time zone
698820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            if (getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) {
699820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Try to find a rule to cover these years
700820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars);
701820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                RRule standardRule = inferRRuleFromCalendars(toStandardCalendars);
702820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if ((daylightRule != null) && (daylightRule.type == RRule.RRULE_DAY_WEEK) &&
703820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                        (standardRule != null) && (standardRule.type == RRule.RRULE_DAY_WEEK)) {
704820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // We need both rules and they have to be DAY/WEEK type
705820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // Write month, day of week, week, hour, minute
706820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET,
707820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            standardRule,
70810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionHour(toStandardCalendars[0]),
70910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionMinute(toStandardCalendars[0]));
710820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET,
711820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                            daylightRule,
71210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionHour(toDaylightCalendars[0]),
71310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                            getTrueTransitionMinute(toDaylightCalendars[0]));
714820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                } else {
715820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // If there's no rule, we'll use the first transition to standard/to daylight
716820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // And indicate that it's just for this year...
717820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    long now = System.currentTimeMillis();
718820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    long standardTransition = findNextTransition(now, toStandardCalendars);
719820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    long daylightTransition = findNextTransition(now, toDaylightCalendars);
720820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    // If we can't find transitions, we can't do DST
721820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    if (standardTransition != 0 && daylightTransition != 0) {
72210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        putTransitionMillisIntoSystemTime(tziBytes,
72310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                                MSFT_TIME_ZONE_STANDARD_DATE_OFFSET, standardTransition);
72410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                        putTransitionMillisIntoSystemTime(tziBytes,
72510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank                                MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET, daylightTransition);
726820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    }
727820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
728377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
729820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            int dstOffset = tz.getDSTSavings();
730820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES);
731377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        }
7327e85a8d17b2b0c80bd04306db5a698ac3b91deaaMakoto Onuki        byte[] tziEncodedBytes = Base64.encode(tziBytes, Base64.NO_WRAP);
733377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        tziString = new String(tziEncodedBytes);
734377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return tziString;
735377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
736377230593dca7cb01483bfaf93959e5821f5f028Marc Blank
737377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
7385862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * Given a String as directly read from EAS, returns a TimeZone corresponding to that String
7395862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @param timeZoneString the String read from the server
7402c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * @param precision the number of milliseconds of precision in TimeZone determination
7415862a85e17e81866ca82a9905577931947fbd44eMarc Blank     * @return the TimeZone, or TimeZone.getDefault() if not found
7425862a85e17e81866ca82a9905577931947fbd44eMarc Blank     */
7432c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    @VisibleForTesting
7442c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static TimeZone tziStringToTimeZone(String timeZoneString, int precision) {
7455862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // If we have this time zone cached, use that value and return
7465862a85e17e81866ca82a9905577931947fbd44eMarc Blank        TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
7475862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (timeZone != null) {
7485862a85e17e81866ca82a9905577931947fbd44eMarc Blank            if (Eas.USER_LOG) {
7495bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                ExchangeService.log(TAG, " Using cached TimeZone " + timeZone.getID());
750377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
751377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        } else {
7522c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank            timeZone = tziStringToTimeZoneImpl(timeZoneString, precision);
753377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            if (timeZone == null) {
754377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                // If we don't find a match, we just return the current TimeZone.  In theory, this
755377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                // shouldn't be happening...
756385a0be662509754e687bcfa9813208b050bf951Marc Blank                ExchangeService.alwaysLog("TimeZone not found using default: " + timeZoneString);
757377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                timeZone = TimeZone.getDefault();
7585862a85e17e81866ca82a9905577931947fbd44eMarc Blank            }
759377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            sTimeZoneCache.put(timeZoneString, timeZone);
7605862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
761377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return timeZone;
762377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
7635862a85e17e81866ca82a9905577931947fbd44eMarc Blank
764377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
7652c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * The standard entry to EAS time zone conversion, using one minute as the precision
7662c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     */
7672c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static public TimeZone tziStringToTimeZone(String timeZoneString) {
7682c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank        return tziStringToTimeZone(timeZoneString, MINUTES);
7692c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    }
7702c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank
7712c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    /**
772377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Given a String as directly read from EAS, tries to find a TimeZone in the database of all
7732c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * time zones that corresponds to that String.  If the test time zone string includes DST and
7742c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * we don't find a match, and we're using standard precision, we try again with lenient
7752c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank     * precision, which is a bit better than guessing
776377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param timeZoneString the String read from the server
77779268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank     * @return the TimeZone, or null if not found
778377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
7792c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank    static TimeZone tziStringToTimeZoneImpl(String timeZoneString, int precision) {
780377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        TimeZone timeZone = null;
7815862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // First, we need to decode the base64 string
7827e85a8d17b2b0c80bd04306db5a698ac3b91deaaMakoto Onuki        byte[] timeZoneBytes = Base64.decode(timeZoneString, Base64.DEFAULT);
7835862a85e17e81866ca82a9905577931947fbd44eMarc Blank
7845862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms
7855862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // but EAS gives us minutes, so do the conversion.  Note that EAS is the bias that's added
7865862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so
7875862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // we need to change the sign
7885862a85e17e81866ca82a9905577931947fbd44eMarc Blank        int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES;
7895862a85e17e81866ca82a9905577931947fbd44eMarc Blank
7905862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return
7915862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // the default time zone
7925862a85e17e81866ca82a9905577931947fbd44eMarc Blank        String[] zoneIds = TimeZone.getAvailableIDs(bias);
7935862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (zoneIds.length > 0) {
7945862a85e17e81866ca82a9905577931947fbd44eMarc Blank            // Try to find an existing TimeZone from the data provided by EAS
7955862a85e17e81866ca82a9905577931947fbd44eMarc Blank            // We start by pulling out the date that standard time begins
7965862a85e17e81866ca82a9905577931947fbd44eMarc Blank            TimeZoneDate dstEnd =
7975862a85e17e81866ca82a9905577931947fbd44eMarc Blank                getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET);
7985862a85e17e81866ca82a9905577931947fbd44eMarc Blank            if (dstEnd == null) {
7995bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // If the default time zone is a match
8005bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                TimeZone defaultTimeZone = TimeZone.getDefault();
8015bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                if (!defaultTimeZone.useDaylightTime() &&
8025bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        ArrayUtils.contains(zoneIds, defaultTimeZone.getID())) {
8035bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    if (Eas.USER_LOG) {
8045bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        ExchangeService.log(TAG, "TimeZone without DST found to be default: " +
8055bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                                defaultTimeZone.getID());
8065bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    }
8075bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    return defaultTimeZone;
8085bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                }
8095862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // In this case, there is no daylight savings time, so the only interesting data
8105bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // for possible matches is the offset and DST availability; we'll take the first
8115bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // match for those
8125bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                for (String zoneId: zoneIds) {
8135bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    timeZone = TimeZone.getTimeZone(zoneId);
8145bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    if (!timeZone.useDaylightTime()) {
8155bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        if (Eas.USER_LOG) {
8165bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                            ExchangeService.log(TAG, "TimeZone without DST found by offset: " +
8175bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                                    timeZone.getID());
8185bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        }
8195bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                        return timeZone;
8205bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                    }
8215862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
8225bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                // None found, return null
8235bb2c4930c27c5385ff1baf033d1f1a97d770d14Marc Blank                return null;
8245862a85e17e81866ca82a9905577931947fbd44eMarc Blank            } else {
825377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes,
8265862a85e17e81866ca82a9905577931947fbd44eMarc Blank                        MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET);
8275862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // See comment above for bias...
8285862a85e17e81866ca82a9905577931947fbd44eMarc Blank                long dstSavings =
829377230593dca7cb01483bfaf93959e5821f5f028Marc Blank                    -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * MINUTES;
8305862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8315862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // We'll go through each time zone to find one with the same DST transitions and
8325862a85e17e81866ca82a9905577931947fbd44eMarc Blank                // savings length
8335862a85e17e81866ca82a9905577931947fbd44eMarc Blank                for (String zoneId: zoneIds) {
8345862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // Get the TimeZone using the zoneId
8355862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    timeZone = TimeZone.getTimeZone(zoneId);
8365862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8375862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // Our strategy here is to check just before and just after the transitions
8385862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // and see whether the check for daylight time matches the expectation
8395862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // If both transitions match, then we have a match for the offset and start/end
8405862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // of dst.  That's the best we can do for now, since there's no other info
8415862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // provided by EAS (i.e. we can't get dynamic transitions, etc.)
8425862a85e17e81866ca82a9905577931947fbd44eMarc Blank
84379268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // Check one minute before and after DST start transition
84479268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    long millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstStart);
8452c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    Date before = new Date(millisAtTransition - precision);
8462c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    Date after = new Date(millisAtTransition + precision);
8475862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (timeZone.inDaylightTime(before)) continue;
8485862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (!timeZone.inDaylightTime(after)) continue;
8495862a85e17e81866ca82a9905577931947fbd44eMarc Blank
85079268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // Check one minute before and after DST end transition
85179268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstEnd);
85279268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // Note that we need to subtract an extra hour here, because we end up with
85379268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    // gaining an hour in the transition BACK to standard time
8542c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    before = new Date(millisAtTransition - (dstSavings + precision));
8552c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    after = new Date(millisAtTransition + precision);
8565862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (!timeZone.inDaylightTime(before)) continue;
8575862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (timeZone.inDaylightTime(after)) continue;
8585862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8595862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // Check that the savings are the same
8605862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    if (dstSavings != timeZone.getDSTSavings()) continue;
86179268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                    return timeZone;
8625862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
863270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                // In this case, there is no daylight savings time, so the only interesting data
864270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                // is the offset, and we know that all of the zoneId's match; we'll take the first
8652c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                boolean lenient = false;
8662c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                if ((dstStart.hour != dstEnd.hour) && (precision == STANDARD_DST_PRECISION)) {
8672c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    timeZone = tziStringToTimeZoneImpl(timeZoneString, LENIENT_DST_PRECISION);
8682c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    lenient = true;
8692c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                } else {
8702c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                    timeZone = TimeZone.getTimeZone(zoneIds[0]);
8712c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                }
872270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                if (Eas.USER_LOG) {
873385a0be662509754e687bcfa9813208b050bf951Marc Blank                    ExchangeService.log(TAG,
8742c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                            "No TimeZone with correct DST settings; using " +
8752c7d44b182654120a98921cbc864be2d135c8fdaMarc Blank                            (lenient ? "lenient" : "first") + ": " + timeZone.getID());
876270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                }
877270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                return timeZone;
8785862a85e17e81866ca82a9905577931947fbd44eMarc Blank            }
8795862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
88079268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        return null;
8815862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
8825862a85e17e81866ca82a9905577931947fbd44eMarc Blank
8835c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank    static public String convertEmailDateTimeToCalendarDateTime(String date) {
8845c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        // Format for email date strings is 2010-02-23T16:00:00.000Z
88579268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank        // Format for calendar date strings is 20100223T160000Z
8865c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank       return date.substring(0, 4) + date.substring(5, 7) + date.substring(8, 13) +
8875c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank           date.substring(14, 16) + date.substring(17, 19) + 'Z';
8885c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank    }
8895c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
890377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static String formatTwo(int num) {
891377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        if (num <= 12) {
892377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            return sTwoCharacterNumbers[num];
893377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        } else
894377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            return Integer.toString(num);
895377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
89614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
897377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
898377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Generate an EAS formatted date/time string based on GMT. See below for details.
899377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
900377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static public String millisToEasDateTime(long millis) {
901c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank        return millisToEasDateTime(millis, sGmtTimeZone, true);
902377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
9035862a85e17e81866ca82a9905577931947fbd44eMarc Blank
904377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
905c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * Generate an EAS formatted local date/time string from a time and a time zone. If the final
906c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * argument is false, only a date will be returned (e.g. 20100331)
907377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param millis a time in milliseconds
908377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param tz a time zone
909c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * @param withTime if the time is to be included in the string
910c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank     * @return an EAS formatted string indicating the date (and time) in the given time zone
911377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
912c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank    static public String millisToEasDateTime(long millis, TimeZone tz, boolean withTime) {
913377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        StringBuilder sb = new StringBuilder();
914377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        GregorianCalendar cal = new GregorianCalendar(tz);
915377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        cal.setTimeInMillis(millis);
916377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sb.append(cal.get(Calendar.YEAR));
917377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
918377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
919c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank        if (withTime) {
920c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append('T');
921c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY)));
922c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append(formatTwo(cal.get(Calendar.MINUTE)));
923c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            sb.append(formatTwo(cal.get(Calendar.SECOND)));
924c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            if (tz == sGmtTimeZone) {
925c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                sb.append('Z');
926c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            }
927820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        }
928820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        return sb.toString();
929820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    }
930820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
931820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank    /**
93210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Return the true minute at which a transition occurs
93310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Our transition time should be the in the minute BEFORE the transition
93410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * If this minute is 59, set minute to 0 and increment the hour
93510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * NOTE: We don't want to add a minute and retrieve minute/hour from the Calendar, because
93610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Calendar time will itself be influenced by the transition!  So adding 1 minute to
93710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * 01:59 (assume PST->PDT) will become 03:00, which isn't what we want (we want 02:00)
93810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     *
93910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @param calendar the calendar holding the transition date/time
94010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @return the true minute of the transition
94110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     */
94279268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static int getTrueTransitionMinute(GregorianCalendar calendar) {
94310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        int minute = calendar.get(Calendar.MINUTE);
94410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        if (minute == 59) {
94510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            minute = 0;
94610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        }
94710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        return minute;
94810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    }
94910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank
95010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    /**
95110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Return the true hour at which a transition occurs
95210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * See description for getTrueTransitionMinute, above
95310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @param calendar the calendar holding the transition date/time
95410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @return the true hour of the transition
95510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     */
95679268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static int getTrueTransitionHour(GregorianCalendar calendar) {
95710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        int hour = calendar.get(Calendar.HOUR_OF_DAY);
95810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        hour++;
95910e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        if (hour == 24) {
96010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank            hour = 0;
96110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        }
96210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        return hour;
96310e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    }
96410e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank
96510e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank    /**
96610e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * Generate a date/time string suitable for VTIMEZONE from a transition time in millis
96710e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * The format is YYYYMMDDTHHMMSS
96810e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank     * @param millis a transition time in milliseconds
969820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param tz a time zone
970820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     * @param dst whether we're entering daylight time
971820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank     */
97279268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank    static String transitionMillisToVCalendarTime(long millis, TimeZone tz, boolean dst) {
973820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        StringBuilder sb = new StringBuilder();
974820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        GregorianCalendar cal = new GregorianCalendar(tz);
975820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        cal.setTimeInMillis(millis);
976820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(cal.get(Calendar.YEAR));
977820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
978820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
979820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank        sb.append('T');
98010e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        sb.append(formatTwo(getTrueTransitionHour(cal)));
98110e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        sb.append(formatTwo(getTrueTransitionMinute(cal)));
98210e1bb12c0e78b60c1302186a724e5617a2ba3bcMarc Blank        sb.append(formatTwo(0));
983377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        return sb.toString();
984377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    }
9855862a85e17e81866ca82a9905577931947fbd44eMarc Blank
986cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank    /**
987270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * Returns a UTC calendar with year/month/day from local calendar and h/m/s/ms = 0
988270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @param time the time in seconds of an all-day event in local time
989270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @return the time in seconds in UTC
990cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank     */
991270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    static public long getUtcAllDayCalendarTime(long time, TimeZone localTimeZone) {
992270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        return transposeAllDayTime(time, localTimeZone, UTC_TIMEZONE);
993270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    }
994270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank
995270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    /**
996270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * Returns a local calendar with year/month/day from UTC calendar and h/m/s/ms = 0
997270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @param time the time in seconds of an all-day event in UTC
998270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     * @return the time in seconds in local time
999270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank     */
1000270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    static public long getLocalAllDayCalendarTime(long time, TimeZone localTimeZone) {
1001270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        return transposeAllDayTime(time, UTC_TIMEZONE, localTimeZone);
1002270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    }
1003270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank
1004270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank    static private long transposeAllDayTime(long time, TimeZone fromTimeZone,
1005270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank            TimeZone toTimeZone) {
1006270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        GregorianCalendar fromCalendar = new GregorianCalendar(fromTimeZone);
1007270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        fromCalendar.setTimeInMillis(time);
1008270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        GregorianCalendar toCalendar = new GregorianCalendar(toTimeZone);
1009cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank        // Set this calendar with correct year, month, and day, but zero hour, minute, and seconds
1010270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        toCalendar.set(fromCalendar.get(GregorianCalendar.YEAR),
1011270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                fromCalendar.get(GregorianCalendar.MONTH),
1012270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank                fromCalendar.get(GregorianCalendar.DATE), 0, 0, 0);
10133baaee079e644467cf18c9b250ac30485f9c54e0Marc Blank        toCalendar.set(GregorianCalendar.MILLISECOND, 0);
1014270a17e49669e0bfc7bd2a6303a684a7acd1266dMarc Blank        return toCalendar.getTimeInMillis();
1015cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank    }
1016cfbbe6bf8cec39204a00d31ee4277b54b0262ba6Marc Blank
1017377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    static void addByDay(StringBuilder rrule, int dow, int wom) {
10185862a85e17e81866ca82a9905577931947fbd44eMarc Blank        rrule.append(";BYDAY=");
10195862a85e17e81866ca82a9905577931947fbd44eMarc Blank        boolean addComma = false;
10205862a85e17e81866ca82a9905577931947fbd44eMarc Blank        for (int i = 0; i < 7; i++) {
10215862a85e17e81866ca82a9905577931947fbd44eMarc Blank            if ((dow & 1) == 1) {
10225862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (addComma) {
10235862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    rrule.append(',');
10245862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
10255862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (wom > 0) {
10265862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // 5 = last week -> -1
10275862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    // So -1SU = last sunday
10285862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    rrule.append(wom == 5 ? -1 : wom);
10295862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
10305862a85e17e81866ca82a9905577931947fbd44eMarc Blank                rrule.append(sDayTokens[i]);
10315862a85e17e81866ca82a9905577931947fbd44eMarc Blank                addComma = true;
10325862a85e17e81866ca82a9905577931947fbd44eMarc Blank            }
10335862a85e17e81866ca82a9905577931947fbd44eMarc Blank            dow >>= 1;
10345862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
10355862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
10365862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1037f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    static void addBySetpos(StringBuilder rrule, int dow, int wom) {
1038f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        // Indicate the days, but don't use wom in this case (it's used in the BYSETPOS);
1039f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        addByDay(rrule, dow, 0);
1040f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        rrule.append(";BYSETPOS=");
1041f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        rrule.append(wom == 5 ? "-1" : wom);
1042f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    }
1043f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank
10445862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static void addByMonthDay(StringBuilder rrule, int dom) {
10455862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // 127 means last day of the month
10465862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (dom == 127) {
10475862a85e17e81866ca82a9905577931947fbd44eMarc Blank            dom = -1;
10485862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
10495862a85e17e81866ca82a9905577931947fbd44eMarc Blank        rrule.append(";BYMONTHDAY=" + dom);
10505862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
10515862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1052f35b67cef20189c12a1a387dedf200eb30089725Marc Blank    /**
1053f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     * Generate the String version of the EAS integer for a given BYDAY value in an rrule
1054f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     * @param dow the BYDAY value of the rrule
1055f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     * @return the String version of the EAS value of these days
1056f35b67cef20189c12a1a387dedf200eb30089725Marc Blank     */
105714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    static String generateEasDayOfWeek(String dow) {
1058f35b67cef20189c12a1a387dedf200eb30089725Marc Blank        int bits = 0;
105914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int bit = 1;
106014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        for (String token: sDayTokens) {
1061f35b67cef20189c12a1a387dedf200eb30089725Marc Blank            // If we can find the day in the dow String, add the bit to our bits value
1062f35b67cef20189c12a1a387dedf200eb30089725Marc Blank            if (dow.indexOf(token) >= 0) {
1063f35b67cef20189c12a1a387dedf200eb30089725Marc Blank                bits |= bit;
106414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            }
1065f35b67cef20189c12a1a387dedf200eb30089725Marc Blank            bit <<= 1;
106614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        }
1067f35b67cef20189c12a1a387dedf200eb30089725Marc Blank        return Integer.toString(bits);
106814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    }
106914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
1070377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
1071377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Extract the value of a token in an RRULE string
1072377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param rrule an RRULE string
1073377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param token a token to look for in the RRULE
1074377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return the value of that token
1075377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
107614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    static String tokenFromRrule(String rrule, String token) {
107714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int start = rrule.indexOf(token);
107814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        if (start < 0) return null;
107914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int len = rrule.length();
108014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        start += token.length();
108114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        int end = start;
108214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        char c;
108314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        do {
108414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            c = rrule.charAt(end++);
1085b129a5f1b340ae6362397685c407099ceae8d9e9Marc Blank            if ((c == ';') || (end == len)) {
108614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                if (end == len) end++;
108714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                return rrule.substring(start, end -1);
108814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            }
1089377230593dca7cb01483bfaf93959e5821f5f028Marc Blank        } while (true);
109014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    }
109114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
109214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    /**
109321c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank     * Reformat an RRULE style UNTIL to an EAS style until
109421c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank     */
109560df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank    @VisibleForTesting
109621c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    static String recurrenceUntilToEasUntil(String until) {
109760df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // Get a calendar in our local time zone
109860df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        GregorianCalendar localCalendar = new GregorianCalendar(TimeZone.getDefault());
109960df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // Set the time per GMT time in the 'until'
110060df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        localCalendar.setTimeInMillis(Utility.parseDateTimeToMillis(until));
110121c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        StringBuilder sb = new StringBuilder();
110260df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // Build a string with local year/month/date
110360df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        sb.append(localCalendar.get(Calendar.YEAR));
110460df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        sb.append(formatTwo(localCalendar.get(Calendar.MONTH) + 1));
110560df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        sb.append(formatTwo(localCalendar.get(Calendar.DAY_OF_MONTH)));
110660df2ad267f1ad7aed45b583adcd1a5bb2a006b0Marc Blank        // EAS ignores the time in 'until'; go figure
1107e3e9ef55a2afd2c121e1f214a8fa34fced3bac38Marc Blank        sb.append("T000000Z");
110821c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        return sb.toString();
110921c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    }
111021c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank
111121c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    /**
1112f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank     * Convenience method to add "count", "interval", and "until" to an EAS calendar stream
1113f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank     * According to EAS docs, OCCURRENCES must always come before INTERVAL
111421c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank     */
1115f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank    static private void addCountIntervalAndUntil(String rrule, Serializer s) throws IOException {
1116f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        String count = tokenFromRrule(rrule, "COUNT=");
1117f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (count != null) {
1118f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            s.data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, count);
1119f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
1120f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        String interval = tokenFromRrule(rrule, "INTERVAL=");
1121f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (interval != null) {
1122f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, interval);
1123f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
112421c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        String until = tokenFromRrule(rrule, "UNTIL=");
112521c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        if (until != null) {
112621c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank            s.data(Tags.CALENDAR_RECURRENCE_UNTIL, recurrenceUntilToEasUntil(until));
112721c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank        }
112821c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    }
112921c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank
1130f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank    static private void addByDay(String byDay, Serializer s) throws IOException {
1131f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        // This can be 1WE (1st Wednesday) or -1FR (last Friday)
1132f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        int weekOfMonth = byDay.charAt(0);
1133f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        String bareByDay;
1134f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (weekOfMonth == '-') {
1135f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            // -1 is the only legal case (last week) Use "5" for EAS
1136f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            weekOfMonth = 5;
1137f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            bareByDay = byDay.substring(2);
1138f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        } else {
1139f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            weekOfMonth = weekOfMonth - '0';
1140f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            bareByDay = byDay.substring(1);
1141f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
1142f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
1143f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
1144f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank    }
1145f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank
1146f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    static private void addByDaySetpos(String byDay, String bySetpos, Serializer s)
1147f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            throws IOException {
1148f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        int weekOfMonth = bySetpos.charAt(0);
1149f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        if (weekOfMonth == '-') {
1150f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            // -1 is the only legal case (last week) Use "5" for EAS
1151f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            weekOfMonth = 5;
1152f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        } else {
1153f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            weekOfMonth = weekOfMonth - '0';
1154f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        }
1155f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
1156f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
1157f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank    }
1158f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank
115921c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank    /**
116014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * Write recurrence information to EAS based on the RRULE in CalendarProvider
116114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @param rrule the RRULE, from CalendarProvider
116214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @param startTime, the DTSTART of this Event
116314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @param s the Serializer we're using to write WBXML data
116414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     * @throws IOException
116514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank     */
116614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    // NOTE: For the moment, we're only parsing recurrence types that are supported by the
1167c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    // Calendar app UI, which is a subset of possible recurrence types
116814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    // This code must be updated when the Calendar adds new functionality
116914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    static public void recurrenceFromRrule(String rrule, long startTime, Serializer s)
117079268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank            throws IOException {
117188683d3ebf9720ee648220dd6884e200d5e87038Marc Blank        if (Eas.USER_LOG) {
1172385a0be662509754e687bcfa9813208b050bf951Marc Blank            ExchangeService.log(TAG, "RRULE: " + rrule);
117388683d3ebf9720ee648220dd6884e200d5e87038Marc Blank        }
117414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        String freq = tokenFromRrule(rrule, "FREQ=");
117514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        // If there's no FREQ=X, then we don't write a recurrence
117614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        // Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the
117714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        // possibility of writing out a partial recurrence stanza
117814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        if (freq != null) {
117914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            if (freq.equals("DAILY")) {
118014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.start(Tags.CALENDAR_RECURRENCE);
118114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0");
1182f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                addCountIntervalAndUntil(rrule, s);
118314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.end();
118414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            } else if (freq.equals("WEEKLY")) {
118514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.start(Tags.CALENDAR_RECURRENCE);
118614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1");
118714045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                // Requires a day of week (whereas RRULE does not)
1188f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                addCountIntervalAndUntil(rrule, s);
118914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byDay = tokenFromRrule(rrule, "BYDAY=");
119014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                if (byDay != null) {
119114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
1192f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    // Find week number (1-4 and 5 for last)
1193f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    if (byDay.startsWith("-1")) {
1194f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, "5");
1195f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    } else {
1196f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        char c = byDay.charAt(0);
1197f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        if (c >= '1' && c <= '4') {
1198f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                            s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, byDay.substring(0, 1));
1199f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        }
1200f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    }
120114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                }
120214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                s.end();
120314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            } else if (freq.equals("MONTHLY")) {
120414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
120514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                if (byMonthDay != null) {
120614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    s.start(Tags.CALENDAR_RECURRENCE);
1207f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    // Special case for last day of month
1208f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    if (byMonthDay == "-1") {
1209f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
1210f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        addCountIntervalAndUntil(rrule, s);
1211f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "127");
1212f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    } else {
1213f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        // The nth day of the month
1214f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
1215f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        addCountIntervalAndUntil(rrule, s);
1216f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
1217f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    }
121814045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    s.end();
121914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                } else {
122014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    String byDay = tokenFromRrule(rrule, "BYDAY=");
1221f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    String bySetpos = tokenFromRrule(rrule, "BYSETPOS=");
122214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    if (byDay != null) {
122314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                        s.start(Tags.CALENDAR_RECURRENCE);
122414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                        s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
1225f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                        addCountIntervalAndUntil(rrule, s);
1226f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        if (bySetpos != null) {
1227f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                            addByDaySetpos(byDay, bySetpos, s);
1228f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        } else {
1229f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                            addByDay(byDay, s);
1230f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                        }
123114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                        s.end();
123214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    }
123314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                }
123414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank            } else if (freq.equals("YEARLY")) {
123514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byMonth = tokenFromRrule(rrule, "BYMONTH=");
123614045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
1237f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                String byDay = tokenFromRrule(rrule, "BYDAY=");
1238f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                if (byMonth == null && byMonthDay == null) {
123914045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    // Calculate the month and day from the startDate
124014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    GregorianCalendar cal = new GregorianCalendar();
124114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    cal.setTimeInMillis(startTime);
124214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    cal.setTimeZone(TimeZone.getDefault());
124314045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1);
124414045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                    byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
124514045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank                }
1246f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                if (byMonth != null && (byMonthDay != null || byDay != null)) {
1247f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.start(Tags.CALENDAR_RECURRENCE);
1248f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.data(Tags.CALENDAR_RECURRENCE_TYPE, byDay == null ? "5" : "6");
1249f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    addCountIntervalAndUntil(rrule, s);
1250f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
1251f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    // Note that both byMonthDay and byDay can't be true in a valid RRULE
1252f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    if (byMonthDay != null) {
1253f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                        s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
1254f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    } else {
1255f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                        addByDay(byDay, s);
1256f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    }
1257f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                    s.end();
1258f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank                }
1259377230593dca7cb01483bfaf93959e5821f5f028Marc Blank            }
126014045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank        }
126114045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank    }
126214045eac56b0aa394ce36f00b4f31dbe8bc1122dMarc Blank
1263377230593dca7cb01483bfaf93959e5821f5f028Marc Blank    /**
1264377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * Build an RRULE String from EAS recurrence information
1265377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param type the type of recurrence
1266377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param occurrences how many recurrences (instances)
1267377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param interval the interval between recurrences
1268377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param dow day of the week
1269377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param dom day of the month
1270377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param wom week of the month
1271377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param moy month of the year
1272377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @param until the last recurrence time
1273377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     * @return a valid RRULE String
1274377230593dca7cb01483bfaf93959e5821f5f028Marc Blank     */
12755862a85e17e81866ca82a9905577931947fbd44eMarc Blank    static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow,
12765862a85e17e81866ca82a9905577931947fbd44eMarc Blank            int dom, int wom, int moy, String until) {
12775862a85e17e81866ca82a9905577931947fbd44eMarc Blank        StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]);
12785862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // INTERVAL and COUNT
12795862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (occurrences > 0) {
12805862a85e17e81866ca82a9905577931947fbd44eMarc Blank            rrule.append(";COUNT=" + occurrences);
12815862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
1282f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        if (interval > 0) {
1283f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank            rrule.append(";INTERVAL=" + interval);
1284f72b4bc0ea714067ae528e63c916f0237d2202e6Marc Blank        }
12855862a85e17e81866ca82a9905577931947fbd44eMarc Blank
12865862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // Days, weeks, months, etc.
12875862a85e17e81866ca82a9905577931947fbd44eMarc Blank        switch(type) {
12885862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 0: // DAILY
12895862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 1: // WEEKLY
1290f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                if (dow > 0) addByDay(rrule, dow, wom);
12915862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
12925862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 2: // MONTHLY
12935862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (dom > 0) addByMonthDay(rrule, dom);
12945862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
12955862a85e17e81866ca82a9905577931947fbd44eMarc Blank            case 3: // MONTHLY (on the nth day)
1296f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                // 127 is a special case meaning "last day of the month"
1297f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                if (dow == 127) {
1298f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    rrule.append(";BYMONTHDAY=-1");
1299f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                // week 5 and dow = weekdays -> last weekday (need BYSETPOS)
1300f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                } else if (wom == 5 && (dow == EAS_WEEKDAYS || dow == EAS_WEEKENDS)) {
1301f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                    addBySetpos(rrule, dow, wom);
1302f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank                } else if (dow > 0) addByDay(rrule, dow, wom);
13035862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
1304820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            case 5: // YEARLY (specific day)
13055862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (dom > 0) addByMonthDay(rrule, dom);
13065862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (moy > 0) {
13075862a85e17e81866ca82a9905577931947fbd44eMarc Blank                    rrule.append(";BYMONTH=" + moy);
13085862a85e17e81866ca82a9905577931947fbd44eMarc Blank                }
13095862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
1310820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            case 6: // YEARLY
13115862a85e17e81866ca82a9905577931947fbd44eMarc Blank                if (dow > 0) addByDay(rrule, dow, wom);
1312820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (dom > 0) addByMonthDay(rrule, dom);
1313820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                if (moy > 0) {
1314820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                    rrule.append(";BYMONTH=" + moy);
1315820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                }
13165862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
13175862a85e17e81866ca82a9905577931947fbd44eMarc Blank            default:
13185862a85e17e81866ca82a9905577931947fbd44eMarc Blank                break;
13195862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
13205862a85e17e81866ca82a9905577931947fbd44eMarc Blank
13215862a85e17e81866ca82a9905577931947fbd44eMarc Blank        // UNTIL comes last
13225862a85e17e81866ca82a9905577931947fbd44eMarc Blank        if (until != null) {
132321c3c670ff6b932a4ecbeda230bb160178bdd957Marc Blank            rrule.append(";UNTIL=" + until);
13245862a85e17e81866ca82a9905577931947fbd44eMarc Blank        }
13255862a85e17e81866ca82a9905577931947fbd44eMarc Blank
1326f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        if (Eas.USER_LOG) {
1327f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank            Log.d(Logging.LOG_TAG, "Created rrule: " + rrule);
1328f352bc9f29cacc61b195069e48d5c8b868660694Marc Blank        }
13295862a85e17e81866ca82a9905577931947fbd44eMarc Blank        return rrule.toString();
13305862a85e17e81866ca82a9905577931947fbd44eMarc Blank    }
133177110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank
133277110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank    /**
133377110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * Create a Calendar in CalendarProvider to which synced Events will be linked
133477110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @param service the sync service requesting Calendar creation
133577110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @param account the account being synced
133677110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @param mailbox the Exchange mailbox for the calendar
133777110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     * @return the unique id of the Calendar
133877110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank     */
133977110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank    static public long createCalendar(EasSyncService service, Account account, Mailbox mailbox) {
134077110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // Create a Calendar object
134177110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        ContentValues cv = new ContentValues();
134277110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // TODO How will this change if the user changes his account display name?
134304c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik        cv.put(Calendars.CALENDAR_DISPLAY_NAME, account.mDisplayName);
13449e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        cv.put(Calendars.ACCOUNT_NAME, account.mEmailAddress);
13459e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        cv.put(Calendars.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
134677110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        cv.put(Calendars.SYNC_EVENTS, 1);
1347443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.VISIBLE, 1);
13485a02f79c91df6df44f3c95742f61f2c25c464cc3Marc Blank        // Don't show attendee status if we're the organizer
1349443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.CAN_ORGANIZER_RESPOND, 0);
1350443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.CAN_MODIFY_TIME_ZONE, 0);
1351443d4f9804a32030446e7a5af7afb3c6df53736fAndy McFadden        cv.put(Calendars.MAX_REMINDERS, 1);
135280a8e57ce2dc2695ed6f35599d326090e4ad9faeRoboErik        cv.put(Calendars.ALLOWED_REMINDERS, ALLOWED_REMINDER_TYPES);
1353937af5abcbc1268f22a3058b00835c74ba20f116RoboErik        cv.put(Calendars.ALLOWED_ATTENDEE_TYPES, ALLOWED_ATTENDEE_TYPES);
1354937af5abcbc1268f22a3058b00835c74ba20f116RoboErik        cv.put(Calendars.ALLOWED_AVAILABILITY, ALLOWED_AVAILABILITIES);
13555a02f79c91df6df44f3c95742f61f2c25c464cc3Marc Blank
135677110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // TODO Coordinate account colors w/ Calendar, if possible
1357d1771c4473a98c032c95ff66fa816043e08976f1Todd Kennedy        int color = new AccountServiceProxy(service.mContext).getAccountColor(account.mId);
13589e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        cv.put(Calendars.CALENDAR_COLOR, color);
135904c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik        cv.put(Calendars.CALENDAR_TIME_ZONE, Time.getCurrentTimezone());
136004c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik        cv.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
136177110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        cv.put(Calendars.OWNER_ACCOUNT, account.mEmailAddress);
136277110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank
13636989716b639d274a98141674556ac9402be32ebeRoboErik        Uri uri = service.mContentResolver.insert(
13646989716b639d274a98141674556ac9402be32ebeRoboErik                asSyncAdapter(Calendars.CONTENT_URI, account.mEmailAddress,
13656989716b639d274a98141674556ac9402be32ebeRoboErik                        Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
136677110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        // We save the id of the calendar into mSyncStatus
136777110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        if (uri != null) {
136877110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank            String stringId = uri.getPathSegments().get(1);
136977110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank            mailbox.mSyncStatus = stringId;
137077110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank            return Long.parseLong(stringId);
137177110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        }
137277110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank        return -1;
137377110d3a646dd691d84abd0b1e083385c1418ac5Marc Blank    }
1374c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
13756989716b639d274a98141674556ac9402be32ebeRoboErik    static Uri asSyncAdapter(Uri uri, String account, String accountType) {
13766989716b639d274a98141674556ac9402be32ebeRoboErik        return uri.buildUpon()
1377693ed7fdd5a7ec7af87d105b76267c78a8acc3dbRoboErik                .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,
1378693ed7fdd5a7ec7af87d105b76267c78a8acc3dbRoboErik                        "true")
13796989716b639d274a98141674556ac9402be32ebeRoboErik                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
13806989716b639d274a98141674556ac9402be32ebeRoboErik                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
13816989716b639d274a98141674556ac9402be32ebeRoboErik    }
13826989716b639d274a98141674556ac9402be32ebeRoboErik
1383b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank    /**
1384b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     * Return the uid for an event based on its globalObjId
1385b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     * @param globalObjId the base64 encoded String provided by EAS
1386b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     * @return the uid for the calendar event
1387b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank     */
1388b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank    static public String getUidFromGlobalObjId(String globalObjId) {
1389b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        StringBuilder sb = new StringBuilder();
1390b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        // First get the decoded base64
1391b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        try {
1392b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            byte[] idBytes = Base64.decode(globalObjId, Base64.DEFAULT);
1393b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            String idString = new String(idBytes);
1394b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // If the base64 decoded string contains the magic substring: "vCal-Uid", then
1395b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // the actual uid is hidden within; the magic substring is never at the start of the
1396b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // decoded base64
1397b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            int index = idString.indexOf("vCal-Uid");
1398b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            if (index > 0) {
1399b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // The uid starts after "vCal-Uidxxxx", where xxxx are padding
1400b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // characters.  And it ends before the last character, which is ascii 0
1401b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                return idString.substring(index + 12, idString.length() - 1);
1402b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            } else {
1403b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // This is an EAS uid. Go through the bytes and write out the hex
1404b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // values as characters; this is what we'll need to pass back to EAS
1405b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                // when responding to the invitation
1406b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                for (byte b: idBytes) {
1407b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                    Utility.byteToHex(sb, b);
1408b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                }
1409b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank                return sb.toString();
1410b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            }
1411b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        } catch (RuntimeException e) {
1412b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            // In the worst of cases (bad format, etc.), we can always return the input
1413b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank            return globalObjId;
1414b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank        }
1415b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank    }
1416b94d16528fc9c7f5dbb5c75c059f76cee2070c09Marc Blank
1417dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    /**
1418dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * Get a selfAttendeeStatus from a busy status
1419dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * The default here is NONE (i.e. we don't know the status)
1420dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * Note that a busy status of FREE must mean NONE as well, since it can't mean declined
1421dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * (there would be no event)
1422dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @param busyStatus the busy status, from EAS
1423dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @return the corresponding value for selfAttendeeStatus
1424dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     */
1425edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank    static public int attendeeStatusFromBusyStatus(int busyStatus) {
1426edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank        int attendeeStatus;
1427dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        switch (busyStatus) {
1428dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case BUSY_STATUS_BUSY:
1429edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
1430dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1431dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case BUSY_STATUS_TENTATIVE:
1432edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
1433dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1434dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case BUSY_STATUS_FREE:
1435e51fedc3c055588a69da56d0b818ea12ed8f706fMarc Blank            case BUSY_STATUS_OUT_OF_OFFICE:
1436dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            default:
1437edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
1438dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        }
1439edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank        return attendeeStatus;
1440dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    }
1441dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank
144265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    /**
1443f9d3d43800dcb522a7c492e96d490eca9f120e43Marc Blank     * Get a selfAttendeeStatus from a response type (EAS 14+)
1444f9d3d43800dcb522a7c492e96d490eca9f120e43Marc Blank     * The default here is NONE (i.e. we don't know the status), though in theory this can't happen
1445f9d3d43800dcb522a7c492e96d490eca9f120e43Marc Blank     * @param busyStatus the response status, from EAS
144665d022dc43e4461e86fd7bc143591f542b07428bMarc Blank     * @return the corresponding value for selfAttendeeStatus
144765d022dc43e4461e86fd7bc143591f542b07428bMarc Blank     */
144865d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    static public int attendeeStatusFromResponseType(int responseType) {
144965d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        int attendeeStatus;
145065d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        switch (responseType) {
145165d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_NOT_RESPONDED:
145265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
145365d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
145465d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_ACCEPTED:
145565d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
145665d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
145765d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_TENTATIVE:
145865d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
145965d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
146065d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            case RESPONSE_TYPE_DECLINED:
146165d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_DECLINED;
146265d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                break;
146365d022dc43e4461e86fd7bc143591f542b07428bMarc Blank            default:
146465d022dc43e4461e86fd7bc143591f542b07428bMarc Blank                attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
146565d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        }
146665d022dc43e4461e86fd7bc143591f542b07428bMarc Blank        return attendeeStatus;
146765d022dc43e4461e86fd7bc143591f542b07428bMarc Blank    }
146865d022dc43e4461e86fd7bc143591f542b07428bMarc Blank
1469dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    /** Get a busy status from a selfAttendeeStatus
1470dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * The default here is BUSY
1471dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @param selfAttendeeStatus from CalendarProvider2
1472dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     * @return the corresponding value of busy status
1473dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank     */
1474edcfd554728a27a7678306ea7432bd8676ec6990Marc Blank    static public int busyStatusFromAttendeeStatus(int selfAttendeeStatus) {
1475dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        int busyStatus;
1476dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        switch (selfAttendeeStatus) {
1477dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_DECLINED:
1478dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_NONE:
1479dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_INVITED:
1480dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                busyStatus = BUSY_STATUS_FREE;
1481dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1482dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_TENTATIVE:
1483dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                busyStatus = BUSY_STATUS_TENTATIVE;
1484dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1485dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            case Attendees.ATTENDEE_STATUS_ACCEPTED:
1486dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank            default:
1487dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                busyStatus = BUSY_STATUS_BUSY;
1488dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank                break;
1489dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        }
1490dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank        return busyStatus;
1491dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank    }
1492dc27937dda50f991de9e12b98b80ee6aa3fe348eMarc Blank
1493dafc866120dac68fabbddcc9943e3995894c5244Marc Blank    static public String buildMessageTextFromEntityValues(Context context,
1494dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            ContentValues entityValues, StringBuilder sb) {
1495dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        if (sb == null) {
1496dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            sb = new StringBuilder();
1497dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        }
1498dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        Resources resources = context.getResources();
1499dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        Date date = new Date(entityValues.getAsLong(Events.DTSTART));
1500dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // TODO: Add more detail to message text
1501dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // Right now, we're using.. When: Tuesday, March 5th at 2:00pm
1502dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // What we're missing is the duration and any recurrence information.  So this should be
1503dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // more like... When: Tuesdays, starting March 5th from 2:00pm - 3:00pm
1504dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // This would require code to build complex strings, and it will have to wait
15059ca8918b82221dd441293973ffb84d565a52993aMarc Blank        // For now, we'll just use the meeting_recurring string
1506f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank
1507f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        boolean allDayEvent = false;
1508f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        if (entityValues.containsKey(Events.ALL_DAY)) {
1509f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
1510f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            allDayEvent = (ade != null) && (ade == 1);
1511f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        }
1512f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        boolean recurringEvent = !entityValues.containsKey(Events.ORIGINAL_SYNC_ID) &&
1513f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            entityValues.containsKey(Events.RRULE);
1514f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank
1515f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        String dateTimeString;
1516f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        int res;
1517f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        if (allDayEvent) {
1518f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            dateTimeString = DateFormat.getDateInstance().format(date);
1519f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            res = recurringEvent ? R.string.meeting_allday_recurring : R.string.meeting_allday;
15209ca8918b82221dd441293973ffb84d565a52993aMarc Blank        } else {
1521f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            dateTimeString = DateFormat.getDateTimeInstance().format(date);
1522f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank            res = recurringEvent ? R.string.meeting_recurring : R.string.meeting_when;
15239ca8918b82221dd441293973ffb84d565a52993aMarc Blank        }
1524f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank        sb.append(resources.getString(res, dateTimeString));
1525f00404a18d7894ed3fba913a356a20812504dbf2Marc Blank
1526dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        String location = null;
1527dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        if (entityValues.containsKey(Events.EVENT_LOCATION)) {
1528dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            location = entityValues.getAsString(Events.EVENT_LOCATION);
1529efae936b117c9e4f3056d52fdbfe4d3f261483e5Marc Blank            if (!TextUtils.isEmpty(location)) {
1530dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                sb.append("\n");
1531dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                sb.append(resources.getString(R.string.meeting_where, location));
1532dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            }
1533dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        }
1534dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        // If there's a description for this event, append it
1535dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        String desc = entityValues.getAsString(Events.DESCRIPTION);
1536dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        if (desc != null) {
1537dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            sb.append("\n--\n");
1538dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            sb.append(desc);
1539dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        }
1540dafc866120dac68fabbddcc9943e3995894c5244Marc Blank        return sb.toString();
1541dafc866120dac68fabbddcc9943e3995894c5244Marc Blank    }
1542dafc866120dac68fabbddcc9943e3995894c5244Marc Blank
1543c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    /**
154424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * Add an attendee to the ics attachment and the to list of the Message being composed
154524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param ics the ics attachment writer
154624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param toList the list of addressees for this email
154724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param attendeeName the name of the attendee
154824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param attendeeEmail the email address of the attendee
154924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param messageFlag the flag indicating the action to be indicated by the message
155024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     * @param account the sending account of the email
155124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank     */
155224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank    static private void addAttendeeToMessage(SimpleIcsWriter ics, ArrayList<Address> toList,
155324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            String attendeeName, String attendeeEmail, int messageFlag, Account account) {
155424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank        if ((messageFlag & Message.FLAG_OUTGOING_MEETING_REQUEST_MASK) != 0) {
155524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            String icalTag = ICALENDAR_ATTENDEE_INVITE;
155624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if ((messageFlag & Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
155724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                icalTag = ICALENDAR_ATTENDEE_CANCEL;
155824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
155924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (attendeeName != null) {
156024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(attendeeName);
156124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
156224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
156324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            toList.add(attendeeName == null ? new Address(attendeeEmail) :
156424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                new Address(attendeeEmail, attendeeName));
156524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank        } else if (attendeeEmail.equalsIgnoreCase(account.mEmailAddress)) {
156624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            String icalTag = null;
156724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            switch (messageFlag) {
156824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                case Message.FLAG_OUTGOING_MEETING_ACCEPT:
156924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag = ICALENDAR_ATTENDEE_ACCEPT;
157024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    break;
157124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                case Message.FLAG_OUTGOING_MEETING_DECLINE:
157224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag = ICALENDAR_ATTENDEE_DECLINE;
157324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    break;
157424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
157524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag = ICALENDAR_ATTENDEE_TENTATIVE;
157624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    break;
157724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
157824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (icalTag != null) {
157924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                if (attendeeName != null) {
158024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                    icalTag += ";CN="
158124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                            + SimpleIcsWriter.quoteParamValue(attendeeName);
158224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                }
158324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
158424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
158524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank        }
158624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank    }
158724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
158824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank    /**
1589c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * Create a Message for an (Event) Entity
1590c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param entity the Entity for the Event (as might be retrieved by CalendarProvider)
1591c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent
1592c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param the unique id of this Event, or null if it can be retrieved from the Event
1593c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param the user's account
1594c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @return a Message with many fields pre-filled (more later)
1595c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     */
1596c8e4352ea6cfa67f15140512e84af8ccede222d2Marc Blank    static public Message createMessageForEntity(Context context, Entity entity,
15975c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            int messageFlag, String uid, Account account) {
1598601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank        return createMessageForEntity(context, entity, messageFlag, uid, account,
159924cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                null /*specifiedAttendee*/);
1600601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    }
1601601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank
1602601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    static public EmailContent.Message createMessageForEntity(Context context, Entity entity,
160324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            int messageFlag, String uid, Account account, String specifiedAttendee) {
1604c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        ContentValues entityValues = entity.getEntityValues();
16055c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        ArrayList<NamedContentValues> subValues = entity.getSubValues();
16069e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik        boolean isException = entityValues.containsKey(Events.ORIGINAL_SYNC_ID);
1607f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank        boolean isReply = false;
1608c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1609c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        EmailContent.Message msg = new EmailContent.Message();
1610c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        msg.mFlags = messageFlag;
1611c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        msg.mTimeStamp = System.currentTimeMillis();
1612c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1613c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        String method;
1614f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank        if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE) != 0) {
1615c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            method = "REQUEST";
1616f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank        } else if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
1617f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            method = "CANCEL";
1618c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        } else {
1619c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            method = "REPLY";
1620f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            isReply = true;
1621c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        }
1622c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
16238d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank        try {
16245c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // Create our iCalendar writer and start generating tags
16255c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            SimpleIcsWriter ics = new SimpleIcsWriter();
16268d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("BEGIN", "VCALENDAR");
16278d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("METHOD", method);
16288d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("PRODID", "AndroidEmail");
16298d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("VERSION", "2.0");
1630820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
1631820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // Our default vcalendar time zone is UTC, but this will change (below) if we're
1632820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // sending a recurring event, in which case we use local time
1633820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            TimeZone vCalendarTimeZone = sGmtTimeZone;
1634c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            String vCalendarDateSuffix = "";
1635c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank
1636c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            // Check for all day event
1637c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            boolean allDayEvent = false;
1638c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            if (entityValues.containsKey(Events.ALL_DAY)) {
1639c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
1640c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                allDayEvent = (ade != null) && (ade == 1);
1641c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                if (allDayEvent) {
1642c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                    // Example: DTSTART;VALUE=DATE:20100331 (all day event)
1643c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                    vCalendarDateSuffix = ";VALUE=DATE";
1644c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                }
1645c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            }
1646820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
1647820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            // If we're inviting people and the meeting is recurring, we need to send our time zone
1648c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank            // information and make sure to send DTSTART/DTEND in local time (unless, of course,
1649b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank            // this is an all-day event).  Recurring, for this purpose, includes exceptions to
1650b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank            // recurring events
1651b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank            if (!isReply && !allDayEvent &&
1652b4f78da94bcc3af7a872b5dc195d5243a948a3c7Marc Blank                    (entityValues.containsKey(Events.RRULE) ||
16539e86eb14c6e1f7d7730f8ca6953fdfd95fe2b143RoboErik                            entityValues.containsKey(Events.ORIGINAL_SYNC_ID))) {
1654820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                vCalendarTimeZone = TimeZone.getDefault();
1655820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                // Write the VTIMEZONE block to the writer
1656820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank                timeZoneToVTimezone(vCalendarTimeZone, ics);
1657c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                // Example: DTSTART;TZID=US/Pacific:20100331T124500
1658c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                vCalendarDateSuffix = ";TZID=" + vCalendarTimeZone.getID();
1659820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            }
1660820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank
16618d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("BEGIN", "VEVENT");
16628d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (uid == null) {
166304c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik                uid = entityValues.getAsString(Events.SYNC_DATA2);
16648d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
16658d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (uid != null) {
16668d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                ics.writeTag("UID", uid);
16678d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
1668c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
16695c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            if (entityValues.containsKey("DTSTAMP")) {
16705c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                ics.writeTag("DTSTAMP", entityValues.getAsString("DTSTAMP"));
16715c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            } else {
167279268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                ics.writeTag("DTSTAMP", millisToEasDateTime(System.currentTimeMillis()));
16738d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
1674c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
16758d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            long startTime = entityValues.getAsLong(Events.DTSTART);
16765c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            if (startTime != 0) {
1677c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                ics.writeTag("DTSTART" + vCalendarDateSuffix,
1678c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                        millisToEasDateTime(startTime, vCalendarTimeZone, !allDayEvent));
16795c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            }
1680c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1681dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            // If this is an Exception, we send the recurrence-id, which is just the original
1682dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            // instance time
1683dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            if (isException) {
1684dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
1685c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                ics.writeTag("RECURRENCE-ID" + vCalendarDateSuffix,
1686c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                        millisToEasDateTime(originalTime, vCalendarTimeZone, !allDayEvent));
1687dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            }
1688dafc866120dac68fabbddcc9943e3995894c5244Marc Blank
16898d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (!entityValues.containsKey(Events.DURATION)) {
16908d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                if (entityValues.containsKey(Events.DTEND)) {
1691c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                    ics.writeTag("DTEND" + vCalendarDateSuffix,
169279268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                            millisToEasDateTime(
1693c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                                    entityValues.getAsLong(Events.DTEND), vCalendarTimeZone,
1694c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                                    !allDayEvent));
16958d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
16968d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            } else {
16978d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // Convert this into millis and add it to DTSTART for DTEND
16988d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // We'll use 1 hour as a default
16998d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                long durationMillis = HOURS;
17008d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                Duration duration = new Duration();
17018d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                try {
17028d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    duration.parse(entityValues.getAsString(Events.DURATION));
170389bee1e3d03b439f4084bc9962bb3cbffd0b878aMarc Blank                    durationMillis = duration.getMillis();
17048d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                } catch (ParseException e) {
17058d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    // We'll use the default in this case
17068d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
1707c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                ics.writeTag("DTEND" + vCalendarDateSuffix,
170879268e63dbda6ebc94d20e72e2bb1c245ee64678Marc Blank                        millisToEasDateTime(
1709c1b63a27ed05bed70f74cf33fe08f8bd1f0d745fMarc Blank                                startTime + durationMillis, vCalendarTimeZone, !allDayEvent));
1710c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1711c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1712dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            String location = null;
17138d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (entityValues.containsKey(Events.EVENT_LOCATION)) {
1714dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                location = entityValues.getAsString(Events.EVENT_LOCATION);
1715dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                ics.writeTag("LOCATION", location);
1716dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            }
1717dafc866120dac68fabbddcc9943e3995894c5244Marc Blank
171804c880a6b5ad041f172d4b1eeecc06d6a06e4141RoboErik            String sequence = entityValues.getAsString(SYNC_VERSION);
1719dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            if (sequence == null) {
1720dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                sequence = "0";
17218d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
17225c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
172346e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            // We'll use 0 to mean a meeting invitation
17245c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            int titleId = 0;
17255c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            switch (messageFlag) {
17265c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_INVITE:
172746e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank                    if (!sequence.equals("0")) {
1728dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                        titleId = R.string.meeting_updated;
1729dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                    }
17305c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
17315c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_ACCEPT:
17325c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    titleId = R.string.meeting_accepted;
17335c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
17345c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_DECLINE:
17355c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    titleId = R.string.meeting_declined;
17365c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
17375c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
17385c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    titleId = R.string.meeting_tentative;
17395c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    break;
174030d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank                case Message.FLAG_OUTGOING_MEETING_CANCEL:
174130d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank                    titleId = R.string.meeting_canceled;
174230d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank                    break;
17435c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            }
1744dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            Resources resources = context.getResources();
17458d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            String title = entityValues.getAsString(Events.TITLE);
17465c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            if (title == null) {
17475c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                title = "";
17488d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
17495c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            ics.writeTag("SUMMARY", title);
175046e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            // For meeting invitations just use the title
175146e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            if (titleId == 0) {
175246e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank                msg.mSubject = title;
175346e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank            } else {
175446e18bd76629be0835a5d5e6c839b6daac6cdfdcMarc Blank                // Otherwise, use the additional text
1755dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                msg.mSubject = resources.getString(titleId, title);
175630d2d4ea74cfca9d27dfd495cebc8387b8f2454dMarc Blank            }
175700702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank
175800702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            // Build the text for the message, starting with an initial line describing the
175900702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            // exception (if this is one)
176000702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            StringBuilder sb = new StringBuilder();
1761f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            if (isException && !isReply) {
176200702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                // Add the line, depending on whether this is a cancellation or update
176300702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                Date date = new Date(entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
176400702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                String dateString = DateFormat.getDateInstance().format(date);
176500702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                if (titleId == R.string.meeting_canceled) {
176600702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                    sb.append(resources.getString(R.string.exception_cancel, dateString));
176700702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                } else {
176800702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                    sb.append(resources.getString(R.string.exception_updated, dateString));
176900702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                }
177000702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                sb.append("\n\n");
177100702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            }
177200702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            String text =
177300702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                CalendarUtilities.buildMessageTextFromEntityValues(context, entityValues, sb);
177400702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank
177500702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            if (text.length() > 0) {
177600702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                ics.writeTag("DESCRIPTION", text);
177700702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            }
177800702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            // And store the message text
177900702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank            msg.mText = text;
1780f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank            if (!isReply) {
17815c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                if (entityValues.containsKey(Events.ALL_DAY)) {
17825c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
17835c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    ics.writeTag("X-MICROSOFT-CDO-ALLDAYEVENT", ade == 0 ? "FALSE" : "TRUE");
17845c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                }
1785c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
17865c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                String rrule = entityValues.getAsString(Events.RRULE);
17875c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                if (rrule != null) {
17885c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                    ics.writeTag("RRULE", rrule);
17895c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank                }
17905c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
179100702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                // If we decide to send alarm information in the meeting request ics file,
179200702b7e577ff2b7b1ae3f6dceefc615ba9cba72Marc Blank                // handle it here by looping through the subvalues
1793c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1794c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1795332d08cc2fc37d9936a73e3a120125c60b0587d1Marc Blank            // Handle attendee data here; determine "to" list and add ATTENDEE tags to ics
17968d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            String organizerName = null;
17978d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            String organizerEmail = null;
17988d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ArrayList<Address> toList = new ArrayList<Address>();
17998d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            for (NamedContentValues ncv: subValues) {
18008d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                Uri ncvUri = ncv.uri;
18018d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                ContentValues ncvValues = ncv.values;
18028d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                if (ncvUri.equals(Attendees.CONTENT_URI)) {
18038d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    Integer relationship =
18048d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
18058d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    // If there's no relationship, we can't create this for EAS
18068d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    // Similarly, we need an attendee email for each invitee
18078d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    if (relationship != null &&
18088d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                            ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
18098d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        // Organizer isn't among attendees in EAS
18108d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
18118d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                            organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
18128d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                            organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
18138d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                            continue;
1814c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                        }
18158d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        String attendeeEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
18168d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
181724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
18188d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        // This shouldn't be possible, but allow for it
18198d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        if (attendeeEmail == null) continue;
182024cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        // If we only want to send to the specifiedAttendee, eliminate others here
182124cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        if ((specifiedAttendee != null) &&
182224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                                !attendeeEmail.equalsIgnoreCase(specifiedAttendee)) {
182324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                            continue;
1824c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                        }
182524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
182624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        addAttendeeToMessage(ics, toList, attendeeName, attendeeEmail, messageFlag,
182724cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                                account);
1828c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                    }
1829c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                }
1830c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1831c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
183224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            // Manually add the specifiedAttendee if he wasn't added in the Attendees loop
183324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (toList.isEmpty() && (specifiedAttendee != null)) {
183424cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                addAttendeeToMessage(ics, toList, null, specifiedAttendee, messageFlag, account);
183524cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            }
183624cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank
18378d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            // Create the organizer tag for ical
18388d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            if (organizerEmail != null) {
18398d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                String icalTag = "ORGANIZER";
18408d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // We should be able to find this, assuming the Email is the user's email
18418d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                // TODO Find this in the account
18428d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                if (organizerName != null) {
1843bc0c8c1523fc0bc42eb82dba6c2977492e441c03Makoto Onuki                    icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(organizerName);
18448d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
18458d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                ics.writeTag(icalTag, "MAILTO:" + organizerEmail);
1846f58e3ba6e6e246a804e6908c831a43b46a61bc07Marc Blank                if (isReply) {
18478d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                    toList.add(organizerName == null ? new Address(organizerEmail) :
18488d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                        new Address(organizerEmail, organizerName));
18498d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                }
1850c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1851c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
185224cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            // If we have no "to" list, we're done
185324cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            if (toList.isEmpty()) return null;
1854601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank
18558d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            // Write out the "to" list
18568d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            Address[] toArray = new Address[toList.size()];
18578d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            int i = 0;
18588d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            for (Address address: toList) {
18598d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank                toArray[i++] = address;
18608d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            }
18618d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            msg.mTo = Address.pack(toArray);
18628d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank
1863820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            ics.writeTag("CLASS", "PUBLIC");
1864dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            ics.writeTag("STATUS", (messageFlag == Message.FLAG_OUTGOING_MEETING_CANCEL) ?
1865dafc866120dac68fabbddcc9943e3995894c5244Marc Blank                    "CANCELLED" : "CONFIRMED");
1866820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            ics.writeTag("TRANSP", "OPAQUE"); // What Exchange uses
1867820dbc5ff3497fdd98fdb1cc42c1d298f9c1f199Marc Blank            ics.writeTag("PRIORITY", "5");  // 1 to 9, 5 = medium
1868dafc866120dac68fabbddcc9943e3995894c5244Marc Blank            ics.writeTag("SEQUENCE", sequence);
18698d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("END", "VEVENT");
18708d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            ics.writeTag("END", "VCALENDAR");
18715c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
18725c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // Create the ics attachment using the "content" field
18735c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            Attachment att = new Attachment();
1874bc0c8c1523fc0bc42eb82dba6c2977492e441c03Makoto Onuki            att.mContentBytes = ics.getBytes();
18755c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            att.mMimeType = "text/calendar; method=" + method;
18765c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            att.mFileName = "invite.ics";
1877e54b75dc4f638e594e9b97e3b4ed8829fbc9b521Makoto Onuki            att.mSize = att.mContentBytes.length;
18785c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // We don't send content-disposition with this attachment
1879b4d217e5170ae397d741e95308d98e80d0c2f637Marc Blank            att.mFlags = Attachment.FLAG_ICS_ALTERNATIVE_PART;
18805c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank
18815c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            // Add the attachment to the message
18825c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            msg.mAttachments = new ArrayList<Attachment>();
18835c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            msg.mAttachments.add(att);
18845c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        } catch (IOException e) {
18855c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank            Log.w(TAG, "IOException in createMessageForEntity");
18868d5c79fe3d792065b72edb6231d51e4301fd2bccMarc Blank            return null;
1887c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        }
1888c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1889c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        // Return the new Message to caller
1890c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        return msg;
1891c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    }
1892c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank
1893c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    /**
18946989716b639d274a98141674556ac9402be32ebeRoboErik     * Create a Message for an Event that can be retrieved from CalendarProvider
18956989716b639d274a98141674556ac9402be32ebeRoboErik     * by its unique id
18966989716b639d274a98141674556ac9402be32ebeRoboErik     *
1897c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param cr a content resolver that can be used to query for the Event
1898c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param eventId the unique id of the Event
18996989716b639d274a98141674556ac9402be32ebeRoboErik     * @param messageFlag the Message.FLAG_XXX constant indicating the type of
19006989716b639d274a98141674556ac9402be32ebeRoboErik     *            email to be sent
19016989716b639d274a98141674556ac9402be32ebeRoboErik     * @param the unique id of this Event, or null if it can be retrieved from
19026989716b639d274a98141674556ac9402be32ebeRoboErik     *            the Event
1903c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @param the user's account
19046989716b639d274a98141674556ac9402be32ebeRoboErik     * @param requireAddressees if true (the default), no Message is returned if
19056989716b639d274a98141674556ac9402be32ebeRoboErik     *            there aren't any addressees; if false, return the Message
19066989716b639d274a98141674556ac9402be32ebeRoboErik     *            regardless (addressees will be filled in later)
1907c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     * @return a Message with many fields pre-filled (more later)
19086989716b639d274a98141674556ac9402be32ebeRoboErik     * @throws RemoteException if there is an issue retrieving the Event from
19096989716b639d274a98141674556ac9402be32ebeRoboErik     *             CalendarProvider
1910c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank     */
19115c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank    static public EmailContent.Message createMessageForEventId(Context context, long eventId,
1912c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            int messageFlag, String uid, Account account) throws RemoteException {
1913601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank        return createMessageForEventId(context, eventId, messageFlag, uid, account,
19146989716b639d274a98141674556ac9402be32ebeRoboErik                null /* specifiedAttendee */);
1915601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    }
1916601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank
1917601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank    static public EmailContent.Message createMessageForEventId(Context context, long eventId,
191824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank            int messageFlag, String uid, Account account, String specifiedAttendee)
1919601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank            throws RemoteException {
19205c6e14ab2f2e4c5dfc97cdeaedcc105159a9f29cMarc Blank        ContentResolver cr = context.getContentResolver();
19216989716b639d274a98141674556ac9402be32ebeRoboErik        EntityIterator eventIterator = EventsEntity.newEntityIterator(cr.query(
19226989716b639d274a98141674556ac9402be32ebeRoboErik                ContentUris.withAppendedId(Events.CONTENT_URI, eventId), null, null, null, null),
19236989716b639d274a98141674556ac9402be32ebeRoboErik                cr);
1924c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        try {
1925c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            while (eventIterator.hasNext()) {
1926c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank                Entity entity = eventIterator.next();
1927601273ad3ff202f50c17061bd2a8fe9492850802Marc Blank                return createMessageForEntity(context, entity, messageFlag, uid, account,
192824cce3b10f1b3ed69de7c4d693e944cab05f8ad2Marc Blank                        specifiedAttendee);
1929c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            }
1930c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        } finally {
1931c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank            eventIterator.close();
1932c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        }
1933c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank        return null;
1934c8dc8009bcbb9dbf781f0028f07b2bbca600aeebMarc Blank    }
1935393208ab154d18a876842777781ab153d34a0281Marc Blank
1936393208ab154d18a876842777781ab153d34a0281Marc Blank    /**
1937393208ab154d18a876842777781ab153d34a0281Marc Blank     * Return a boolean value for an integer ContentValues column
1938393208ab154d18a876842777781ab153d34a0281Marc Blank     * @param values a ContentValues object
1939393208ab154d18a876842777781ab153d34a0281Marc Blank     * @param columnName the name of a column to be found in the ContentValues
1940393208ab154d18a876842777781ab153d34a0281Marc Blank     * @return a boolean representation of the value of columnName in values; null and 0 = false,
1941393208ab154d18a876842777781ab153d34a0281Marc Blank     * other integers = true
1942393208ab154d18a876842777781ab153d34a0281Marc Blank     */
1943393208ab154d18a876842777781ab153d34a0281Marc Blank    static public boolean getIntegerValueAsBoolean(ContentValues values, String columnName) {
1944393208ab154d18a876842777781ab153d34a0281Marc Blank        Integer intValue = values.getAsInteger(columnName);
1945393208ab154d18a876842777781ab153d34a0281Marc Blank        return (intValue != null && intValue != 0);
1946393208ab154d18a876842777781ab153d34a0281Marc Blank    }
19478e26c42accbaf72eff6694173496aba0e6aa37f6Mihai Preda}
1948