• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.calendarcommon2;
17 
18 import java.text.SimpleDateFormat;
19 import java.util.Calendar;
20 import java.util.GregorianCalendar;
21 import java.util.Locale;
22 import java.util.TimeZone;
23 
24 /**
25  * Helper class to make migration out of android.text.format.Time smoother.
26  */
27 public class Time {
28 
29     public static final String TIMEZONE_UTC = "UTC";
30 
31     private static final int EPOCH_JULIAN_DAY = 2440588;
32     private static final long HOUR_IN_MILLIS = 60 * 60 * 1000;
33     private static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
34 
35     private static final String FORMAT_ALL_DAY_PATTERN = "yyyyMMdd";
36     private static final String FORMAT_TIME_PATTERN = "yyyyMMdd'T'HHmmss";
37     private static final String FORMAT_TIME_UTC_PATTERN = "yyyyMMdd'T'HHmmss'Z'";
38     private static final String FORMAT_LOG_TIME_PATTERN = "EEE, MMM dd, yyyy hh:mm a";
39 
40     /*
41      * Define symbolic constants for accessing the fields in this class. Used in
42      * getActualMaximum().
43      */
44     public static final int SECOND = 1;
45     public static final int MINUTE = 2;
46     public static final int HOUR = 3;
47     public static final int MONTH_DAY = 4;
48     public static final int MONTH = 5;
49     public static final int YEAR = 6;
50     public static final int WEEK_DAY = 7;
51     public static final int YEAR_DAY = 8;
52     public static final int WEEK_NUM = 9;
53 
54     public static final int SUNDAY = 0;
55     public static final int MONDAY = 1;
56     public static final int TUESDAY = 2;
57     public static final int WEDNESDAY = 3;
58     public static final int THURSDAY = 4;
59     public static final int FRIDAY = 5;
60     public static final int SATURDAY = 6;
61 
62     private final GregorianCalendar mCalendar;
63 
64     private int year;
65     private int month;
66     private int monthDay;
67     private int hour;
68     private int minute;
69     private int second;
70 
71     private int yearDay;
72     private int weekDay;
73 
74     private String timezone;
75     private boolean allDay;
76 
77     /**
78      * Enabling this flag will apply appropriate dst transition logic when calling either
79      * {@code toMillis()} or {@code normalize()} and their respective *ApplyDst() equivalents. <br>
80      * When this flag is enabled, the following calls would be considered equivalent:
81      * <ul>
82      *     <li>{@code a.t.f.Time#normalize(true)} and {@code #normalize()}</li>
83      *     <li>{@code a.t.f.Time#toMillis(true)} and {@code #toMillis()}</li>
84      *     <li>{@code a.t.f.Time#normalize(false)} and {@code #normalizeApplyDst()}</li>
85      *     <li>{@code a.t.f.Time#toMillis(false)} and {@code #toMillisApplyDst()}</li>
86      * </ul>
87      * When the flag is disabled, both {@code toMillis()} and {@code normalize()} will ignore any
88      * dst transitions unless minutes or hours were added to the time (the default behavior of the
89      * a.t.f.Time class). <br>
90      *
91      * NOTE: currently, this flag is disabled because there are no direct manipulations of the day,
92      * hour, or minute fields. All of the accesses are correctly done via setters and they rely on
93      * a private normalize call in their respective classes to achieve their expected behavior.
94      * Additionally, using any of the {@code #set()} methods or {@code #parse()} will result in
95      * normalizing by ignoring DST, which is what the default behavior is for the a.t.f.Time class.
96      */
97     static final boolean APPLY_DST_CHANGE_LOGIC = false;
98     private int mDstChangedByField = -1;
99 
Time()100     public Time() {
101         this(TimeZone.getDefault().getID());
102     }
103 
Time(String timezone)104     public Time(String timezone) {
105         if (timezone == null) {
106             throw new NullPointerException("timezone cannot be null.");
107         }
108         this.timezone = timezone;
109         // Although the process's default locale is used here, #clear() will explicitly set the
110         // first day of the week to MONDAY to match with the expected a.t.f.Time implementation.
111         mCalendar = new GregorianCalendar(getTimeZone(), Locale.getDefault());
112         clear(this.timezone);
113     }
114 
readFieldsFromCalendar()115     private void readFieldsFromCalendar() {
116         year = mCalendar.get(Calendar.YEAR);
117         month = mCalendar.get(Calendar.MONTH);
118         monthDay = mCalendar.get(Calendar.DAY_OF_MONTH);
119         hour = mCalendar.get(Calendar.HOUR_OF_DAY);
120         minute = mCalendar.get(Calendar.MINUTE);
121         second = mCalendar.get(Calendar.SECOND);
122     }
123 
writeFieldsToCalendar()124     private void writeFieldsToCalendar() {
125         clearCalendar();
126         mCalendar.set(year, month, monthDay, hour, minute, second);
127         mCalendar.set(Calendar.MILLISECOND, 0);
128     }
129 
isInDst()130     private boolean isInDst() {
131         return mCalendar.getTimeZone().inDaylightTime(mCalendar.getTime());
132     }
133 
add(int field, int amount)134     public void add(int field, int amount) {
135         final boolean wasDstBefore = isInDst();
136         mCalendar.add(getCalendarField(field), amount);
137         if (APPLY_DST_CHANGE_LOGIC && wasDstBefore != isInDst()
138                 && (field == MONTH_DAY || field == HOUR || field == MINUTE)) {
139             mDstChangedByField = field;
140         }
141     }
142 
set(long millis)143     public void set(long millis) {
144         clearCalendar();
145         mCalendar.setTimeInMillis(millis);
146         readFieldsFromCalendar();
147     }
148 
set(Time other)149     public void set(Time other) {
150         clearCalendar();
151         mCalendar.setTimeZone(other.getTimeZone());
152         mCalendar.setTimeInMillis(other.mCalendar.getTimeInMillis());
153         readFieldsFromCalendar();
154     }
155 
set(int day, int month, int year)156     public void set(int day, int month, int year) {
157         clearCalendar();
158         mCalendar.set(year, month, day);
159         readFieldsFromCalendar();
160     }
161 
set(int second, int minute, int hour, int day, int month, int year)162     public void set(int second, int minute, int hour, int day, int month, int year) {
163         clearCalendar();
164         mCalendar.set(year, month, day, hour, minute, second);
165         readFieldsFromCalendar();
166     }
167 
setJulianDay(int julianDay)168     public long setJulianDay(int julianDay) {
169         long millis = (julianDay - EPOCH_JULIAN_DAY) * DAY_IN_MILLIS;
170         mCalendar.setTimeInMillis(millis);
171         readFieldsFromCalendar();
172 
173         // adjust day approximation, set the time to 12am, and re-normalize
174         monthDay += julianDay - getJulianDay(millis, getGmtOffset());
175         hour = 0;
176         minute = 0;
177         second = 0;
178         writeFieldsToCalendar();
179         return normalize();
180     }
181 
getJulianDay(long begin, long gmtOff)182     public static int getJulianDay(long begin, long gmtOff) {
183         return android.text.format.Time.getJulianDay(begin, gmtOff);
184     }
185 
getWeekNumber()186     public int getWeekNumber() {
187         return mCalendar.get(Calendar.WEEK_OF_YEAR);
188     }
189 
getCalendarField(int field)190     private int getCalendarField(int field) {
191         switch (field) {
192             case SECOND: return Calendar.SECOND;
193             case MINUTE: return Calendar.MINUTE;
194             case HOUR: return Calendar.HOUR_OF_DAY;
195             case MONTH_DAY: return Calendar.DAY_OF_MONTH;
196             case MONTH: return Calendar.MONTH;
197             case YEAR: return Calendar.YEAR;
198             case WEEK_DAY: return Calendar.DAY_OF_WEEK;
199             case YEAR_DAY: return Calendar.DAY_OF_YEAR;
200             case WEEK_NUM: return Calendar.WEEK_OF_YEAR;
201             default:
202                 throw new RuntimeException("bad field=" + field);
203         }
204     }
205 
getActualMaximum(int field)206     public int getActualMaximum(int field) {
207         return mCalendar.getActualMaximum(getCalendarField(field));
208     }
209 
switchTimezone(String timezone)210     public void switchTimezone(String timezone) {
211         long msBefore = mCalendar.getTimeInMillis();
212         mCalendar.setTimeZone(TimeZone.getTimeZone(timezone));
213         mCalendar.setTimeInMillis(msBefore);
214         mDstChangedByField = -1;
215         readFieldsFromCalendar();
216     }
217 
218     /**
219      * @param apply whether to apply dst logic on the ms or not; if apply is true, it is equivalent
220      *              to calling the normalize or toMillis APIs in a.t.f.Time with ignoreDst=false
221      */
getDstAdjustedMillis(boolean apply, long ms)222     private long getDstAdjustedMillis(boolean apply, long ms) {
223         if (APPLY_DST_CHANGE_LOGIC) {
224             if (apply && mDstChangedByField == MONTH_DAY) {
225                 return isInDst() ? (ms + HOUR_IN_MILLIS) : (ms - HOUR_IN_MILLIS);
226             } else if (!apply && (mDstChangedByField == HOUR || mDstChangedByField == MINUTE)) {
227                 return isInDst() ? (ms - HOUR_IN_MILLIS) : (ms + HOUR_IN_MILLIS);
228             }
229         }
230         return ms;
231     }
232 
normalizeInternal()233     private long normalizeInternal() {
234         final long ms = mCalendar.getTimeInMillis();
235         readFieldsFromCalendar();
236         return ms;
237     }
238 
normalize()239     public long normalize() {
240         return getDstAdjustedMillis(false, normalizeInternal());
241     }
242 
normalizeApplyDst()243     long normalizeApplyDst() {
244         return getDstAdjustedMillis(true, normalizeInternal());
245     }
246 
parse(String time)247     public void parse(String time) {
248         if (time == null) {
249             throw new NullPointerException("time string is null");
250         }
251         parseInternal(time);
252         writeFieldsToCalendar();
253     }
254 
format2445()255     public String format2445() {
256         writeFieldsToCalendar();
257         final SimpleDateFormat sdf = new SimpleDateFormat(
258                 allDay ? FORMAT_ALL_DAY_PATTERN
259                        : (TIMEZONE_UTC.equals(getTimezone()) ? FORMAT_TIME_UTC_PATTERN
260                                                              : FORMAT_TIME_PATTERN));
261         sdf.setTimeZone(getTimeZone());
262         return sdf.format(mCalendar.getTime());
263     }
264 
toMillis()265     public long toMillis() {
266         return getDstAdjustedMillis(false, mCalendar.getTimeInMillis());
267     }
268 
toMillisApplyDst()269     long toMillisApplyDst() {
270         return getDstAdjustedMillis(true, mCalendar.getTimeInMillis());
271     }
272 
getTimeZone()273     private TimeZone getTimeZone() {
274         return timezone != null ? TimeZone.getTimeZone(timezone) : TimeZone.getDefault();
275     }
276 
compareTo(Time other)277     public int compareTo(Time other) {
278         return mCalendar.compareTo(other.mCalendar);
279     }
280 
clearCalendar()281     private void clearCalendar() {
282         mDstChangedByField = -1;
283         mCalendar.clear();
284         mCalendar.set(Calendar.HOUR_OF_DAY, 0); // HOUR_OF_DAY doesn't get reset with #clear
285         mCalendar.setTimeZone(getTimeZone());
286         // set fields for week number computation according to ISO 8601.
287         mCalendar.setFirstDayOfWeek(Calendar.MONDAY);
288         mCalendar.setMinimalDaysInFirstWeek(4);
289     }
290 
clear(String timezoneId)291     public void clear(String timezoneId) {
292         clearCalendar();
293         readFieldsFromCalendar();
294         setTimezone(timezoneId);
295     }
296 
getYear()297     public int getYear() {
298         return mCalendar.get(Calendar.YEAR);
299     }
300 
setYear(int year)301     public void setYear(int year) {
302         this.year = year;
303         mCalendar.set(Calendar.YEAR, year);
304     }
305 
getMonth()306     public int getMonth() {
307         return mCalendar.get(Calendar.MONTH);
308     }
309 
setMonth(int month)310     public void setMonth(int month) {
311         this.month = month;
312         mCalendar.set(Calendar.MONTH, month);
313     }
314 
getDay()315     public int getDay() {
316         return mCalendar.get(Calendar.DAY_OF_MONTH);
317     }
318 
setDay(int day)319     public void setDay(int day) {
320         this.monthDay = day;
321         mCalendar.set(Calendar.DAY_OF_MONTH, day);
322     }
323 
getHour()324     public int getHour() {
325         return mCalendar.get(Calendar.HOUR_OF_DAY);
326     }
327 
setHour(int hour)328     public void setHour(int hour) {
329         this.hour = hour;
330         mCalendar.set(Calendar.HOUR_OF_DAY, hour);
331     }
332 
getMinute()333     public int getMinute() {
334         return mCalendar.get(Calendar.MINUTE);
335     }
336 
setMinute(int minute)337     public void setMinute(int minute) {
338         this.minute = minute;
339         mCalendar.set(Calendar.MINUTE, minute);
340     }
341 
getSecond()342     public int getSecond() {
343         return mCalendar.get(Calendar.SECOND);
344     }
345 
setSecond(int second)346     public void setSecond(int second) {
347         this.second = second;
348         mCalendar.set(Calendar.SECOND, second);
349     }
350 
getTimezone()351     public String getTimezone() {
352         return mCalendar.getTimeZone().getID();
353     }
354 
setTimezone(String timezone)355     public void setTimezone(String timezone) {
356         this.timezone = timezone;
357         mCalendar.setTimeZone(getTimeZone());
358     }
359 
getYearDay()360     public int getYearDay() {
361         // yearDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1.
362         return mCalendar.get(Calendar.DAY_OF_YEAR) - 1;
363     }
364 
setYearDay(int yearDay)365     public void setYearDay(int yearDay) {
366         this.yearDay = yearDay;
367         // yearDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1.
368         mCalendar.set(Calendar.DAY_OF_YEAR, yearDay + 1);
369     }
370 
getWeekDay()371     public int getWeekDay() {
372         // weekDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1.
373         return mCalendar.get(Calendar.DAY_OF_WEEK) - 1;
374     }
375 
setWeekDay(int weekDay)376     public void setWeekDay(int weekDay) {
377         this.weekDay = weekDay;
378         // weekDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1.
379         mCalendar.set(Calendar.DAY_OF_WEEK, weekDay + 1);
380     }
381 
isAllDay()382     public boolean isAllDay() {
383         return allDay;
384     }
385 
setAllDay(boolean allDay)386     public void setAllDay(boolean allDay) {
387         this.allDay = allDay;
388     }
389 
getGmtOffset()390     public long getGmtOffset() {
391         return mCalendar.getTimeZone().getOffset(mCalendar.getTimeInMillis()) / 1000;
392     }
393 
parseInternal(String s)394     private void parseInternal(String s) {
395         int len = s.length();
396         if (len < 8) {
397             throw new IllegalArgumentException("String is too short: \"" + s +
398                     "\" Expected at least 8 characters.");
399         } else if (len > 8 && len < 15) {
400             throw new IllegalArgumentException("String is too short: \"" + s
401                     + "\" If there are more than 8 characters there must be at least 15.");
402         }
403 
404         // year
405         int n = getChar(s, 0, 1000);
406         n += getChar(s, 1, 100);
407         n += getChar(s, 2, 10);
408         n += getChar(s, 3, 1);
409         year = n;
410 
411         // month
412         n = getChar(s, 4, 10);
413         n += getChar(s, 5, 1);
414         n--;
415         month = n;
416 
417         // day of month
418         n = getChar(s, 6, 10);
419         n += getChar(s, 7, 1);
420         monthDay = n;
421 
422         if (len > 8) {
423             checkChar(s, 8, 'T');
424             allDay = false;
425 
426             // hour
427             n = getChar(s, 9, 10);
428             n += getChar(s, 10, 1);
429             hour = n;
430 
431             // min
432             n = getChar(s, 11, 10);
433             n += getChar(s, 12, 1);
434             minute = n;
435 
436             // sec
437             n = getChar(s, 13, 10);
438             n += getChar(s, 14, 1);
439             second = n;
440 
441             if (len > 15) {
442                 // Z
443                 checkChar(s, 15, 'Z');
444                 timezone = TIMEZONE_UTC;
445             }
446         } else {
447             allDay = true;
448             hour = 0;
449             minute = 0;
450             second = 0;
451         }
452 
453         weekDay = 0;
454         yearDay = 0;
455     }
456 
checkChar(String s, int spos, char expected)457     private void checkChar(String s, int spos, char expected) {
458         final char c = s.charAt(spos);
459         if (c != expected) {
460             throw new IllegalArgumentException(String.format(
461                     "Unexpected character 0x%02d at pos=%d.  Expected 0x%02d (\'%c\').",
462                     (int) c, spos, (int) expected, expected));
463         }
464     }
465 
getChar(String s, int spos, int mul)466     private int getChar(String s, int spos, int mul) {
467         final char c = s.charAt(spos);
468         if (Character.isDigit(c)) {
469             return Character.getNumericValue(c) * mul;
470         } else {
471             throw new IllegalArgumentException("Parse error at pos=" + spos);
472         }
473     }
474 
475     // NOTE: only used for outputting time to error logs
format()476     public String format() {
477         final SimpleDateFormat sdf =
478                 new SimpleDateFormat(FORMAT_LOG_TIME_PATTERN, Locale.getDefault());
479         return sdf.format(mCalendar.getTime());
480     }
481 
482     // NOTE: only used in tests
parse3339(String time)483     public boolean parse3339(String time) {
484         android.text.format.Time tmp = generateInstance();
485         boolean success = tmp.parse3339(time);
486         copyAndWriteInstance(tmp);
487         return success;
488     }
489 
490     // NOTE: only used in tests
format3339(boolean allDay)491     public String format3339(boolean allDay) {
492         return generateInstance().format3339(allDay);
493     }
494 
generateInstance()495     private android.text.format.Time generateInstance() {
496         android.text.format.Time tmp = new android.text.format.Time(timezone);
497         tmp.set(second, minute, hour, monthDay, month, year);
498 
499         tmp.yearDay = yearDay;
500         tmp.weekDay = weekDay;
501 
502         tmp.timezone = timezone;
503         tmp.gmtoff = getGmtOffset();
504         tmp.allDay = allDay;
505         tmp.set(mCalendar.getTimeInMillis());
506         if (tmp.allDay && (tmp.hour != 0 || tmp.minute != 0 || tmp.second != 0)) {
507             // Time SDK expects hour, minute, second to be 0 if allDay is true
508             tmp.hour = 0;
509             tmp.minute = 0;
510             tmp.second = 0;
511         }
512 
513         return tmp;
514     }
515 
copyAndWriteInstance(android.text.format.Time time)516     private void copyAndWriteInstance(android.text.format.Time time) {
517         year = time.year;
518         month = time.month;
519         monthDay = time.monthDay;
520         hour = time.hour;
521         minute = time.minute;
522         second = time.second;
523 
524         yearDay = time.yearDay;
525         weekDay = time.weekDay;
526 
527         timezone = time.timezone;
528         allDay = time.allDay;
529 
530         writeFieldsToCalendar();
531     }
532 }
533