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