diff options
Diffstat (limited to 'src/com/android/calendarcommon2/Time.java')
-rw-r--r-- | src/com/android/calendarcommon2/Time.java | 532 |
1 files changed, 532 insertions, 0 deletions
diff --git a/src/com/android/calendarcommon2/Time.java b/src/com/android/calendarcommon2/Time.java new file mode 100644 index 0000000..f0af248 --- /dev/null +++ b/src/com/android/calendarcommon2/Time.java @@ -0,0 +1,532 @@ +/* + * Copyright (C) 2020 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.calendarcommon2; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Helper class to make migration out of android.text.format.Time smoother. + */ +public class Time { + + public static final String TIMEZONE_UTC = "UTC"; + + private static final int EPOCH_JULIAN_DAY = 2440588; + private static final long HOUR_IN_MILLIS = 60 * 60 * 1000; + private static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS; + + private static final String FORMAT_ALL_DAY_PATTERN = "yyyyMMdd"; + private static final String FORMAT_TIME_PATTERN = "yyyyMMdd'T'HHmmss"; + private static final String FORMAT_TIME_UTC_PATTERN = "yyyyMMdd'T'HHmmss'Z'"; + private static final String FORMAT_LOG_TIME_PATTERN = "EEE, MMM dd, yyyy hh:mm a"; + + /* + * Define symbolic constants for accessing the fields in this class. Used in + * getActualMaximum(). + */ + public static final int SECOND = 1; + public static final int MINUTE = 2; + public static final int HOUR = 3; + public static final int MONTH_DAY = 4; + public static final int MONTH = 5; + public static final int YEAR = 6; + public static final int WEEK_DAY = 7; + public static final int YEAR_DAY = 8; + public static final int WEEK_NUM = 9; + + public static final int SUNDAY = 0; + public static final int MONDAY = 1; + public static final int TUESDAY = 2; + public static final int WEDNESDAY = 3; + public static final int THURSDAY = 4; + public static final int FRIDAY = 5; + public static final int SATURDAY = 6; + + private final GregorianCalendar mCalendar; + + private int year; + private int month; + private int monthDay; + private int hour; + private int minute; + private int second; + + private int yearDay; + private int weekDay; + + private String timezone; + private boolean allDay; + + /** + * Enabling this flag will apply appropriate dst transition logic when calling either + * {@code toMillis()} or {@code normalize()} and their respective *ApplyDst() equivalents. <br> + * When this flag is enabled, the following calls would be considered equivalent: + * <ul> + * <li>{@code a.t.f.Time#normalize(true)} and {@code #normalize()}</li> + * <li>{@code a.t.f.Time#toMillis(true)} and {@code #toMillis()}</li> + * <li>{@code a.t.f.Time#normalize(false)} and {@code #normalizeApplyDst()}</li> + * <li>{@code a.t.f.Time#toMillis(false)} and {@code #toMillisApplyDst()}</li> + * </ul> + * When the flag is disabled, both {@code toMillis()} and {@code normalize()} will ignore any + * dst transitions unless minutes or hours were added to the time (the default behavior of the + * a.t.f.Time class). <br> + * + * NOTE: currently, this flag is disabled because there are no direct manipulations of the day, + * hour, or minute fields. All of the accesses are correctly done via setters and they rely on + * a private normalize call in their respective classes to achieve their expected behavior. + * Additionally, using any of the {@code #set()} methods or {@code #parse()} will result in + * normalizing by ignoring DST, which is what the default behavior is for the a.t.f.Time class. + */ + static final boolean APPLY_DST_CHANGE_LOGIC = false; + private int mDstChangedByField = -1; + + public Time() { + this(TimeZone.getDefault().getID()); + } + + public Time(String timezone) { + if (timezone == null) { + throw new NullPointerException("timezone cannot be null."); + } + this.timezone = timezone; + // Although the process's default locale is used here, #clear() will explicitly set the + // first day of the week to MONDAY to match with the expected a.t.f.Time implementation. + mCalendar = new GregorianCalendar(getTimeZone(), Locale.getDefault()); + clear(this.timezone); + } + + private void readFieldsFromCalendar() { + year = mCalendar.get(Calendar.YEAR); + month = mCalendar.get(Calendar.MONTH); + monthDay = mCalendar.get(Calendar.DAY_OF_MONTH); + hour = mCalendar.get(Calendar.HOUR_OF_DAY); + minute = mCalendar.get(Calendar.MINUTE); + second = mCalendar.get(Calendar.SECOND); + } + + private void writeFieldsToCalendar() { + clearCalendar(); + mCalendar.set(year, month, monthDay, hour, minute, second); + mCalendar.set(Calendar.MILLISECOND, 0); + } + + private boolean isInDst() { + return mCalendar.getTimeZone().inDaylightTime(mCalendar.getTime()); + } + + public void add(int field, int amount) { + final boolean wasDstBefore = isInDst(); + mCalendar.add(getCalendarField(field), amount); + if (APPLY_DST_CHANGE_LOGIC && wasDstBefore != isInDst() + && (field == MONTH_DAY || field == HOUR || field == MINUTE)) { + mDstChangedByField = field; + } + } + + public void set(long millis) { + clearCalendar(); + mCalendar.setTimeInMillis(millis); + readFieldsFromCalendar(); + } + + public void set(Time other) { + clearCalendar(); + mCalendar.setTimeZone(other.getTimeZone()); + mCalendar.setTimeInMillis(other.mCalendar.getTimeInMillis()); + readFieldsFromCalendar(); + } + + public void set(int day, int month, int year) { + clearCalendar(); + mCalendar.set(year, month, day); + readFieldsFromCalendar(); + } + + public void set(int second, int minute, int hour, int day, int month, int year) { + clearCalendar(); + mCalendar.set(year, month, day, hour, minute, second); + readFieldsFromCalendar(); + } + + public long setJulianDay(int julianDay) { + long millis = (julianDay - EPOCH_JULIAN_DAY) * DAY_IN_MILLIS; + mCalendar.setTimeInMillis(millis); + readFieldsFromCalendar(); + + // adjust day approximation, set the time to 12am, and re-normalize + monthDay += julianDay - getJulianDay(millis, getGmtOffset()); + hour = 0; + minute = 0; + second = 0; + writeFieldsToCalendar(); + return normalize(); + } + + public static int getJulianDay(long begin, long gmtOff) { + return android.text.format.Time.getJulianDay(begin, gmtOff); + } + + public int getWeekNumber() { + return mCalendar.get(Calendar.WEEK_OF_YEAR); + } + + private int getCalendarField(int field) { + switch (field) { + case SECOND: return Calendar.SECOND; + case MINUTE: return Calendar.MINUTE; + case HOUR: return Calendar.HOUR_OF_DAY; + case MONTH_DAY: return Calendar.DAY_OF_MONTH; + case MONTH: return Calendar.MONTH; + case YEAR: return Calendar.YEAR; + case WEEK_DAY: return Calendar.DAY_OF_WEEK; + case YEAR_DAY: return Calendar.DAY_OF_YEAR; + case WEEK_NUM: return Calendar.WEEK_OF_YEAR; + default: + throw new RuntimeException("bad field=" + field); + } + } + + public int getActualMaximum(int field) { + return mCalendar.getActualMaximum(getCalendarField(field)); + } + + public void switchTimezone(String timezone) { + long msBefore = mCalendar.getTimeInMillis(); + mCalendar.setTimeZone(TimeZone.getTimeZone(timezone)); + mCalendar.setTimeInMillis(msBefore); + mDstChangedByField = -1; + readFieldsFromCalendar(); + } + + /** + * @param apply whether to apply dst logic on the ms or not; if apply is true, it is equivalent + * to calling the normalize or toMillis APIs in a.t.f.Time with ignoreDst=false + */ + private long getDstAdjustedMillis(boolean apply, long ms) { + if (APPLY_DST_CHANGE_LOGIC) { + if (apply && mDstChangedByField == MONTH_DAY) { + return isInDst() ? (ms + HOUR_IN_MILLIS) : (ms - HOUR_IN_MILLIS); + } else if (!apply && (mDstChangedByField == HOUR || mDstChangedByField == MINUTE)) { + return isInDst() ? (ms - HOUR_IN_MILLIS) : (ms + HOUR_IN_MILLIS); + } + } + return ms; + } + + private long normalizeInternal() { + final long ms = mCalendar.getTimeInMillis(); + readFieldsFromCalendar(); + return ms; + } + + public long normalize() { + return getDstAdjustedMillis(false, normalizeInternal()); + } + + long normalizeApplyDst() { + return getDstAdjustedMillis(true, normalizeInternal()); + } + + public void parse(String time) { + if (time == null) { + throw new NullPointerException("time string is null"); + } + parseInternal(time); + writeFieldsToCalendar(); + } + + public String format2445() { + writeFieldsToCalendar(); + final SimpleDateFormat sdf = new SimpleDateFormat( + allDay ? FORMAT_ALL_DAY_PATTERN + : (TIMEZONE_UTC.equals(getTimezone()) ? FORMAT_TIME_UTC_PATTERN + : FORMAT_TIME_PATTERN)); + sdf.setTimeZone(getTimeZone()); + return sdf.format(mCalendar.getTime()); + } + + public long toMillis() { + return getDstAdjustedMillis(false, mCalendar.getTimeInMillis()); + } + + long toMillisApplyDst() { + return getDstAdjustedMillis(true, mCalendar.getTimeInMillis()); + } + + private TimeZone getTimeZone() { + return timezone != null ? TimeZone.getTimeZone(timezone) : TimeZone.getDefault(); + } + + public int compareTo(Time other) { + return mCalendar.compareTo(other.mCalendar); + } + + private void clearCalendar() { + mDstChangedByField = -1; + mCalendar.clear(); + mCalendar.set(Calendar.HOUR_OF_DAY, 0); // HOUR_OF_DAY doesn't get reset with #clear + mCalendar.setTimeZone(getTimeZone()); + // set fields for week number computation according to ISO 8601. + mCalendar.setFirstDayOfWeek(Calendar.MONDAY); + mCalendar.setMinimalDaysInFirstWeek(4); + } + + public void clear(String timezoneId) { + clearCalendar(); + readFieldsFromCalendar(); + setTimezone(timezoneId); + } + + public int getYear() { + return mCalendar.get(Calendar.YEAR); + } + + public void setYear(int year) { + this.year = year; + mCalendar.set(Calendar.YEAR, year); + } + + public int getMonth() { + return mCalendar.get(Calendar.MONTH); + } + + public void setMonth(int month) { + this.month = month; + mCalendar.set(Calendar.MONTH, month); + } + + public int getDay() { + return mCalendar.get(Calendar.DAY_OF_MONTH); + } + + public void setDay(int day) { + this.monthDay = day; + mCalendar.set(Calendar.DAY_OF_MONTH, day); + } + + public int getHour() { + return mCalendar.get(Calendar.HOUR_OF_DAY); + } + + public void setHour(int hour) { + this.hour = hour; + mCalendar.set(Calendar.HOUR_OF_DAY, hour); + } + + public int getMinute() { + return mCalendar.get(Calendar.MINUTE); + } + + public void setMinute(int minute) { + this.minute = minute; + mCalendar.set(Calendar.MINUTE, minute); + } + + public int getSecond() { + return mCalendar.get(Calendar.SECOND); + } + + public void setSecond(int second) { + this.second = second; + mCalendar.set(Calendar.SECOND, second); + } + + public String getTimezone() { + return mCalendar.getTimeZone().getID(); + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + mCalendar.setTimeZone(getTimeZone()); + } + + public int getYearDay() { + // yearDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1. + return mCalendar.get(Calendar.DAY_OF_YEAR) - 1; + } + + public void setYearDay(int yearDay) { + this.yearDay = yearDay; + // yearDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1. + mCalendar.set(Calendar.DAY_OF_YEAR, yearDay + 1); + } + + public int getWeekDay() { + // weekDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1. + return mCalendar.get(Calendar.DAY_OF_WEEK) - 1; + } + + public void setWeekDay(int weekDay) { + this.weekDay = weekDay; + // weekDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1. + mCalendar.set(Calendar.DAY_OF_WEEK, weekDay + 1); + } + + public boolean isAllDay() { + return allDay; + } + + public void setAllDay(boolean allDay) { + this.allDay = allDay; + } + + public long getGmtOffset() { + return mCalendar.getTimeZone().getOffset(mCalendar.getTimeInMillis()) / 1000; + } + + private void parseInternal(String s) { + int len = s.length(); + if (len < 8) { + throw new IllegalArgumentException("String is too short: \"" + s + + "\" Expected at least 8 characters."); + } else if (len > 8 && len < 15) { + throw new IllegalArgumentException("String is too short: \"" + s + + "\" If there are more than 8 characters there must be at least 15."); + } + + // year + int n = getChar(s, 0, 1000); + n += getChar(s, 1, 100); + n += getChar(s, 2, 10); + n += getChar(s, 3, 1); + year = n; + + // month + n = getChar(s, 4, 10); + n += getChar(s, 5, 1); + n--; + month = n; + + // day of month + n = getChar(s, 6, 10); + n += getChar(s, 7, 1); + monthDay = n; + + if (len > 8) { + checkChar(s, 8, 'T'); + allDay = false; + + // hour + n = getChar(s, 9, 10); + n += getChar(s, 10, 1); + hour = n; + + // min + n = getChar(s, 11, 10); + n += getChar(s, 12, 1); + minute = n; + + // sec + n = getChar(s, 13, 10); + n += getChar(s, 14, 1); + second = n; + + if (len > 15) { + // Z + checkChar(s, 15, 'Z'); + timezone = TIMEZONE_UTC; + } + } else { + allDay = true; + hour = 0; + minute = 0; + second = 0; + } + + weekDay = 0; + yearDay = 0; + } + + private void checkChar(String s, int spos, char expected) { + final char c = s.charAt(spos); + if (c != expected) { + throw new IllegalArgumentException(String.format( + "Unexpected character 0x%02d at pos=%d. Expected 0x%02d (\'%c\').", + (int) c, spos, (int) expected, expected)); + } + } + + private int getChar(String s, int spos, int mul) { + final char c = s.charAt(spos); + if (Character.isDigit(c)) { + return Character.getNumericValue(c) * mul; + } else { + throw new IllegalArgumentException("Parse error at pos=" + spos); + } + } + + // NOTE: only used for outputting time to error logs + public String format() { + final SimpleDateFormat sdf = + new SimpleDateFormat(FORMAT_LOG_TIME_PATTERN, Locale.getDefault()); + return sdf.format(mCalendar.getTime()); + } + + // NOTE: only used in tests + public boolean parse3339(String time) { + android.text.format.Time tmp = generateInstance(); + boolean success = tmp.parse3339(time); + copyAndWriteInstance(tmp); + return success; + } + + // NOTE: only used in tests + public String format3339(boolean allDay) { + return generateInstance().format3339(allDay); + } + + private android.text.format.Time generateInstance() { + android.text.format.Time tmp = new android.text.format.Time(timezone); + tmp.set(second, minute, hour, monthDay, month, year); + + tmp.yearDay = yearDay; + tmp.weekDay = weekDay; + + tmp.timezone = timezone; + tmp.gmtoff = getGmtOffset(); + tmp.allDay = allDay; + tmp.set(mCalendar.getTimeInMillis()); + if (tmp.allDay && (tmp.hour != 0 || tmp.minute != 0 || tmp.second != 0)) { + // Time SDK expects hour, minute, second to be 0 if allDay is true + tmp.hour = 0; + tmp.minute = 0; + tmp.second = 0; + } + + return tmp; + } + + private void copyAndWriteInstance(android.text.format.Time time) { + year = time.year; + month = time.month; + monthDay = time.monthDay; + hour = time.hour; + minute = time.minute; + second = time.second; + + yearDay = time.yearDay; + weekDay = time.weekDay; + + timezone = time.timezone; + allDay = time.allDay; + + writeFieldsToCalendar(); + } +} |