| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.exchange.utility; |
| |
| import com.android.exchange.Eas; |
| |
| import org.bouncycastle.util.encoders.Base64; |
| |
| import android.util.Log; |
| |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.GregorianCalendar; |
| import java.util.HashMap; |
| import java.util.TimeZone; |
| |
| public class CalendarUtilities { |
| // NOTE: Most definitions in this class are have package visibility for testing purposes |
| private static final String TAG = "CalendarUtility"; |
| |
| // Time related convenience constants, in milliseconds |
| static final int SECONDS = 1000; |
| static final int MINUTES = SECONDS*60; |
| static final int HOURS = MINUTES*60; |
| |
| // NOTE All Microsoft data structures are little endian |
| |
| // The following constants relate to standard Microsoft data sizes |
| // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx |
| static final int MSFT_LONG_SIZE = 4; |
| static final int MSFT_WCHAR_SIZE = 2; |
| static final int MSFT_WORD_SIZE = 2; |
| |
| // The following constants relate to Microsoft's SYSTEMTIME structure |
| // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4 |
| |
| static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE; |
| static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE; |
| static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE; |
| static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE; |
| static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE; |
| static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE; |
| //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE; |
| //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE; |
| static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE; |
| |
| // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure |
| // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx |
| static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0; |
| static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET = |
| MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE; |
| static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET = |
| MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32); |
| static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET = |
| MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE; |
| static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET = |
| MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE; |
| static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET = |
| MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32); |
| static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET = |
| MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE; |
| static final int MSFT_TIME_ZONE_SIZE = |
| MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE; |
| |
| // TimeZone cache; we parse/decode as little as possible, because the process is quite slow |
| private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>(); |
| |
| // There is no type 4 (thus, the "") |
| static final String[] sTypeToFreq = |
| new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"}; |
| |
| static final String[] sDayTokens = |
| new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"}; |
| |
| static final String[] sTwoCharacterNumbers = |
| new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"}; |
| |
| // Return a 4-byte long from a byte array (little endian) |
| static int getLong(byte[] bytes, int offset) { |
| return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) | |
| ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24); |
| } |
| |
| // Put a 4-byte long into a byte array (little endian) |
| static void setLong(byte[] bytes, int offset, int value) { |
| bytes[offset++] = (byte) (value & 0xFF); |
| bytes[offset++] = (byte) ((value >> 8) & 0xFF); |
| bytes[offset++] = (byte) ((value >> 16) & 0xFF); |
| bytes[offset] = (byte) ((value >> 24) & 0xFF); |
| } |
| |
| // Return a 2-byte word from a byte array (little endian) |
| static int getWord(byte[] bytes, int offset) { |
| return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8); |
| } |
| |
| // Put a 2-byte word into a byte array (little endian) |
| static void setWord(byte[] bytes, int offset, int value) { |
| bytes[offset++] = (byte) (value & 0xFF); |
| bytes[offset] = (byte) ((value >> 8) & 0xFF); |
| } |
| |
| // Internal structure for storing a time zone date from a SYSTEMTIME structure |
| // This date represents either the start or the end time for DST |
| static class TimeZoneDate { |
| String year; |
| int month; |
| int dayOfWeek; |
| int day; |
| int time; |
| int hour; |
| int minute; |
| } |
| |
| // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset |
| static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) { |
| TimeZoneDate tzd = new TimeZoneDate(); |
| |
| // MSFT year is an int; TimeZone is a String |
| int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR); |
| tzd.year = Integer.toString(num); |
| |
| // MSFT month = 0 means no daylight time |
| // MSFT months are 1 based; TimeZone is 0 based |
| num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH); |
| if (num == 0) { |
| return null; |
| } else { |
| tzd.month = num -1; |
| } |
| |
| // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1 |
| tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1; |
| |
| // Get the "day" in TimeZone format |
| num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY); |
| // 5 means "last" in MSFT land; for TimeZone, it's -1 |
| if (num == 5) { |
| tzd.day = -1; |
| } else { |
| tzd.day = num; |
| } |
| |
| // Turn hours/minutes into ms from midnight (per TimeZone) |
| int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR); |
| tzd.hour = hour; |
| int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE); |
| tzd.minute = minute; |
| tzd.time = (hour*HOURS) + (minute*MINUTES); |
| |
| return tzd; |
| } |
| |
| // Return a String from within a byte array at the given offset with max characters |
| // Unused for now, but might be helpful for debugging |
| // String getString(byte[] bytes, int offset, int max) { |
| // StringBuilder sb = new StringBuilder(); |
| // while (max-- > 0) { |
| // int b = bytes[offset]; |
| // if (b == 0) break; |
| // sb.append((char)b); |
| // offset += 2; |
| // } |
| // return sb.toString(); |
| // } |
| |
| /** |
| * Build a GregorianCalendar, based on a time zone and TimeZoneDate. |
| * @param timeZone the time zone we're checking |
| * @param tzd the TimeZoneDate we're interested in |
| * @return a GregorianCalendar with the given time zone and date |
| */ |
| static GregorianCalendar getCheckCalendar(TimeZone timeZone, TimeZoneDate tzd) { |
| GregorianCalendar testCalendar = new GregorianCalendar(timeZone); |
| testCalendar.set(GregorianCalendar.YEAR, 2009); |
| testCalendar.set(GregorianCalendar.MONTH, tzd.month); |
| testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek); |
| testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day); |
| testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour); |
| testCalendar.set(GregorianCalendar.MINUTE, tzd.minute); |
| return testCalendar; |
| } |
| |
| /** |
| * Given a String as directly read from EAS, returns a TimeZone corresponding to that String |
| * @param timeZoneString the String read from the server |
| * @return the TimeZone, or TimeZone.getDefault() if not found |
| */ |
| static public TimeZone parseTimeZone(String timeZoneString) { |
| // If we have this time zone cached, use that value and return |
| TimeZone timeZone = sTimeZoneCache.get(timeZoneString); |
| if (timeZone != null) { |
| if (Eas.USER_LOG) { |
| Log.d(TAG, "TimeZone " + timeZone.getID() + " in cache: " + timeZone.getDisplayName()); |
| } |
| return timeZone; |
| } |
| |
| // First, we need to decode the base64 string |
| byte[] timeZoneBytes = Base64.decode(timeZoneString); |
| |
| // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms |
| // but EAS gives us minutes, so do the conversion. Note that EAS is the bias that's added |
| // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so |
| // we need to change the sign |
| int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES; |
| |
| // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return |
| // the default time zone |
| String[] zoneIds = TimeZone.getAvailableIDs(bias); |
| if (zoneIds.length > 0) { |
| // Try to find an existing TimeZone from the data provided by EAS |
| // We start by pulling out the date that standard time begins |
| TimeZoneDate dstEnd = |
| getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET); |
| if (dstEnd == null) { |
| // In this case, there is no daylight savings time, so the only interesting data |
| // is the offset, and we know that all of the zoneId's match; we'll take the first |
| timeZone = TimeZone.getTimeZone(zoneIds[0]); |
| String dn = timeZone.getDisplayName(); |
| sTimeZoneCache.put(timeZoneString, timeZone); |
| if (Eas.USER_LOG) { |
| Log.d(TAG, "TimeZone without DST found by offset: " + dn); |
| } |
| return timeZone; |
| } else { |
| TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes, |
| MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET); |
| // See comment above for bias... |
| long dstSavings = |
| -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * 60*SECONDS; |
| |
| // We'll go through each time zone to find one with the same DST transitions and |
| // savings length |
| for (String zoneId: zoneIds) { |
| // Get the TimeZone using the zoneId |
| timeZone = TimeZone.getTimeZone(zoneId); |
| |
| // Our strategy here is to check just before and just after the transitions |
| // and see whether the check for daylight time matches the expectation |
| // If both transitions match, then we have a match for the offset and start/end |
| // of dst. That's the best we can do for now, since there's no other info |
| // provided by EAS (i.e. we can't get dynamic transitions, etc.) |
| |
| // Check start DST transition |
| GregorianCalendar testCalendar = getCheckCalendar(timeZone, dstStart); |
| testCalendar.add(GregorianCalendar.MINUTE, -1); |
| Date before = testCalendar.getTime(); |
| testCalendar.add(GregorianCalendar.MINUTE, 2); |
| Date after = testCalendar.getTime(); |
| if (timeZone.inDaylightTime(before)) continue; |
| if (!timeZone.inDaylightTime(after)) continue; |
| |
| // Check end DST transition |
| testCalendar = getCheckCalendar(timeZone, dstEnd); |
| testCalendar.add(GregorianCalendar.HOUR, -2); |
| before = testCalendar.getTime(); |
| testCalendar.add(GregorianCalendar.HOUR, 2); |
| after = testCalendar.getTime(); |
| if (!timeZone.inDaylightTime(before)) continue; |
| if (timeZone.inDaylightTime(after)) continue; |
| |
| // Check that the savings are the same |
| if (dstSavings != timeZone.getDSTSavings()) continue; |
| |
| // If we're here, it's the right time zone, modulo dynamic DST |
| String dn = timeZone.getDisplayName(); |
| sTimeZoneCache.put(timeZoneString, timeZone); |
| if (Eas.USER_LOG) { |
| Log.d(TAG, "TimeZone found by rules: " + dn); |
| } |
| return timeZone; |
| } |
| } |
| } |
| // If we don't find a match, we just return the current TimeZone. In theory, this |
| // shouldn't be happening... |
| Log.w(TAG, "TimeZone not found with bias = " + bias + ", using default."); |
| return TimeZone.getDefault(); |
| } |
| |
| /** |
| * Generate a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone |
| * ID that might be found in an Event. For now, we'll just use the standard bias, and we'll |
| * tackle DST later |
| * @param name the name of the TimeZone |
| * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element |
| */ |
| static public String timeZoneToTZIString(String name) { |
| // TODO Handle DST (ugh) |
| TimeZone tz = TimeZone.getTimeZone(name); |
| byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE]; |
| |
| int standardBias = - tz.getRawOffset(); |
| standardBias /= 60*SECONDS; |
| setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias); |
| |
| byte[] tziEncodedBytes = Base64.encode(tziBytes); |
| return new String(tziEncodedBytes); |
| } |
| |
| /** |
| * Generate a time in milliseconds from a date string that represents a date/time in GMT |
| * @param DateTime string from Exchange server |
| * @return the time in milliseconds (since Jan 1, 1970) |
| */ |
| static public long parseDateTime(String date) { |
| // Format for calendar date strings is 20090211T180303Z |
| GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)), |
| Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)), |
| Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)), |
| Integer.parseInt(date.substring(13, 15))); |
| cal.setTimeZone(TimeZone.getTimeZone("GMT")); |
| return cal.getTimeInMillis(); |
| } |
| |
| static String formatTwo(int num) { |
| if (num <= 12) { |
| return sTwoCharacterNumbers[num]; |
| } else |
| return Integer.toString(num); |
| } |
| |
| static public String millisToEasDateTime(long millis) { |
| StringBuilder sb = new StringBuilder(); |
| GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); |
| cal.setTimeInMillis(millis); |
| sb.append(cal.get(Calendar.YEAR)); |
| sb.append(formatTwo(cal.get(Calendar.MONTH) + 1)); |
| sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH))); |
| sb.append('T'); |
| sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY))); |
| sb.append(formatTwo(cal.get(Calendar.MINUTE))); |
| sb.append(formatTwo(cal.get(Calendar.SECOND))); |
| sb.append('Z'); |
| return sb.toString(); |
| } |
| |
| static void addByDay(StringBuilder rrule, int dow, int wom) { |
| rrule.append(";BYDAY="); |
| boolean addComma = false; |
| for (int i = 0; i < 7; i++) { |
| if ((dow & 1) == 1) { |
| if (addComma) { |
| rrule.append(','); |
| } |
| if (wom > 0) { |
| // 5 = last week -> -1 |
| // So -1SU = last sunday |
| rrule.append(wom == 5 ? -1 : wom); |
| } |
| rrule.append(sDayTokens[i]); |
| addComma = true; |
| } |
| dow >>= 1; |
| } |
| } |
| |
| static void addByMonthDay(StringBuilder rrule, int dom) { |
| // 127 means last day of the month |
| if (dom == 127) { |
| dom = -1; |
| } |
| rrule.append(";BYMONTHDAY=" + dom); |
| } |
| |
| static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow, |
| int dom, int wom, int moy, String until) { |
| StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]); |
| |
| // INTERVAL and COUNT |
| if (interval > 0) { |
| rrule.append(";INTERVAL=" + interval); |
| } |
| if (occurrences > 0) { |
| rrule.append(";COUNT=" + occurrences); |
| } |
| |
| // Days, weeks, months, etc. |
| switch(type) { |
| case 0: // DAILY |
| case 1: // WEEKLY |
| if (dow > 0) addByDay(rrule, dow, -1); |
| break; |
| case 2: // MONTHLY |
| if (dom > 0) addByMonthDay(rrule, dom); |
| break; |
| case 3: // MONTHLY (on the nth day) |
| if (dow > 0) addByDay(rrule, dow, wom); |
| break; |
| case 5: // YEARLY |
| if (dom > 0) addByMonthDay(rrule, dom); |
| if (moy > 0) { |
| // TODO MAKE SURE WE'RE 1 BASED |
| rrule.append(";BYMONTH=" + moy); |
| } |
| break; |
| case 6: // YEARLY (on the nth day) |
| if (dow > 0) addByDay(rrule, dow, wom); |
| if (moy > 0) addByMonthDay(rrule, dow); |
| break; |
| default: |
| break; |
| } |
| |
| // UNTIL comes last |
| // TODO Add UNTIL code |
| if (until != null) { |
| // *** until probably needs reformatting |
| //rrule.append(";UNTIL=" + until); |
| } |
| |
| return rrule.toString(); |
| } |
| } |