TimeZoneInfo.java revision 0717b65fee21de6a321e42c9f3852f8b622c3215
1/*
2 * Copyright (C) 2013 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
17package com.android.timezonepicker;
18
19import android.content.Context;
20import android.text.format.DateUtils;
21import android.text.format.Time;
22import android.util.Log;
23import android.util.SparseArray;
24
25import java.lang.reflect.Field;
26import java.text.DateFormat;
27import java.util.Arrays;
28import java.util.Date;
29import java.util.Formatter;
30import java.util.Locale;
31import java.util.TimeZone;
32
33public class TimeZoneInfo implements Comparable<TimeZoneInfo> {
34    private static final char SEPARATOR = ',';
35    private static final String TAG = null;
36    public static int NUM_OF_TRANSITIONS = 6;
37    public static long time = System.currentTimeMillis() / 1000;
38    public static boolean is24HourFormat;
39
40    TimeZone mTz;
41    public String mTzId;
42    int mRawoffset;
43    public int[] mTransitions; // may have trailing 0's.
44    public String mCountry;
45    public int groupId;
46    private boolean hasDst;
47    public String mDisplayName;
48    private Time recycledTime = new Time();
49    private static StringBuilder mSB = new StringBuilder(50);
50    private static Formatter mFormatter = new Formatter(mSB, Locale.getDefault());
51
52    public TimeZoneInfo(TimeZone tz, String country) {
53        mTz = tz;
54        mTzId = tz.getID();
55        mCountry = country;
56        mRawoffset = tz.getRawOffset();
57        hasDst = tz.useDaylightTime();
58
59        try {
60            mTransitions = getTransitions(tz, time);
61        } catch (NoSuchFieldException ignored) {
62        } catch (IllegalAccessException ignored) {
63            ignored.printStackTrace();
64        }
65    }
66
67    SparseArray<String> mLocalTimeCache = new SparseArray<String>();
68    long mLocalTimeCacheReferenceTime = 0;
69    static private long mGmtDisplayNameUpdateTime;
70    static private SparseArray<String> mGmtDisplayNameCache = new SparseArray<String>();
71
72    public String getLocalTime(long referenceTime) {
73        recycledTime.timezone = TimeZone.getDefault().getID();
74        recycledTime.set(referenceTime);
75
76        int currYearDay = recycledTime.year * 366 + recycledTime.yearDay;
77
78        recycledTime.timezone = mTzId;
79        recycledTime.set(referenceTime);
80
81        String localTimeStr = null;
82
83        int hourMinute = recycledTime.hour * 60 +
84                recycledTime.minute;
85
86        if (mLocalTimeCacheReferenceTime != referenceTime) {
87            mLocalTimeCacheReferenceTime = referenceTime;
88            mLocalTimeCache.clear();
89        } else {
90            localTimeStr = mLocalTimeCache.get(hourMinute);
91        }
92
93        if (localTimeStr == null) {
94            String format = "%I:%M %p";
95            if (currYearDay != (recycledTime.year * 366 + recycledTime.yearDay)) {
96                if (is24HourFormat) {
97                    format = "%b %d %H:%M";
98                } else {
99                    format = "%b %d %I:%M %p";
100                }
101            } else if (is24HourFormat) {
102                format = "%H:%M";
103            }
104
105            // format = "%Y-%m-%d %H:%M";
106            localTimeStr = recycledTime.format(format);
107            mLocalTimeCache.put(hourMinute, localTimeStr);
108        }
109
110        return localTimeStr;
111    }
112
113    public int getLocalHr(long referenceTime) {
114        recycledTime.timezone = mTzId;
115        recycledTime.set(referenceTime);
116        return recycledTime.hour;
117    }
118
119    public int getNowOffsetMillis() {
120        return mTz.getOffset(System.currentTimeMillis());
121    }
122
123    /*
124     * The method is synchronized because there's one mSB, which is used by
125     * mFormatter, per instance. If there are multiple callers for
126     * getGmtDisplayName, the output may be mangled.
127     */
128    public synchronized String getGmtDisplayName(Context context) {
129        // TODO Note: The local time is shown in current time (current GMT
130        // offset) which may be different from the time specified by
131        // mTimeMillis
132
133        final long nowMinute = System.currentTimeMillis() / DateUtils.MINUTE_IN_MILLIS;
134        final long now = nowMinute * DateUtils.MINUTE_IN_MILLIS;
135        final int gmtOffset = mTz.getOffset(now);
136        int cacheKey;
137
138        boolean hasFutureDST = mTz.useDaylightTime();
139        if (hasFutureDST) {
140            cacheKey = (int) (gmtOffset + 36 * DateUtils.HOUR_IN_MILLIS);
141        } else {
142            cacheKey = (int) (gmtOffset - 36 * DateUtils.HOUR_IN_MILLIS);
143        }
144
145        String displayName = null;
146        if (mGmtDisplayNameUpdateTime != nowMinute) {
147            mGmtDisplayNameUpdateTime = nowMinute;
148            mGmtDisplayNameCache.clear();
149        } else {
150            displayName = mGmtDisplayNameCache.get(cacheKey);
151        }
152
153        if (displayName == null) {
154            mSB.setLength(0);
155            int flags = DateUtils.FORMAT_ABBREV_ALL;
156            flags |= DateUtils.FORMAT_SHOW_TIME;
157            if (TimeZoneInfo.is24HourFormat) {
158                flags |= DateUtils.FORMAT_24HOUR;
159            }
160
161            // mFormatter writes to mSB
162            DateUtils.formatDateRange(context, mFormatter, now, now, flags, mTzId);
163            mSB.append(" (GMT");
164
165            if (gmtOffset < 0) {
166                mSB.append('-');
167            } else {
168                mSB.append('+');
169            }
170
171            final int p = Math.abs(gmtOffset);
172            mSB.append(p / DateUtils.HOUR_IN_MILLIS); // Hour
173
174            final int min = (p / 60000) % 60;
175            if (min != 0) { // Show minutes if non-zero
176                mSB.append(':');
177                if (min < 10) {
178                    mSB.append('0');
179                }
180                mSB.append(min);
181            }
182            mSB.append(')');
183
184            if (hasFutureDST) {
185                mSB.append(" \u2600"); // Sun symbol
186            }
187
188            displayName = mSB.toString();
189            mGmtDisplayNameCache.put(cacheKey, displayName);
190        }
191        return displayName;
192    }
193
194    private static int[] getTransitions(TimeZone tz, long time)
195            throws IllegalAccessException, NoSuchFieldException {
196        Class<?> zoneInfoClass = tz.getClass();
197        Field mTransitionsField = zoneInfoClass.getDeclaredField("mTransitions");
198        mTransitionsField.setAccessible(true);
199        int[] objTransitions = (int[]) mTransitionsField.get(tz);
200        int[] transitions = null;
201        if (objTransitions.length != 0) {
202            transitions = new int[NUM_OF_TRANSITIONS];
203            int numOfTransitions = 0;
204            for (int i = 0; i < objTransitions.length; ++i) {
205                if (objTransitions[i] < time) {
206                    continue;
207                }
208                transitions[numOfTransitions++] = objTransitions[i];
209                if (numOfTransitions == NUM_OF_TRANSITIONS) {
210                    break;
211                }
212            }
213        }
214        return transitions;
215    }
216
217    public boolean hasSameRules(TimeZoneInfo tzi) {
218        // this.mTz.hasSameRules(tzi.mTz)
219
220        return this.mRawoffset == tzi.mRawoffset
221                && Arrays.equals(this.mTransitions, tzi.mTransitions);
222    }
223
224    @Override
225    public String toString() {
226        StringBuilder sb = new StringBuilder();
227
228        final String country = this.mCountry;
229        final TimeZone tz = this.mTz;
230
231        sb.append(mTzId);
232        sb.append(SEPARATOR);
233        sb.append(tz.getDisplayName(false /* daylightTime */, TimeZone.LONG));
234        sb.append(SEPARATOR);
235        sb.append(tz.getDisplayName(false /* daylightTime */, TimeZone.SHORT));
236        sb.append(SEPARATOR);
237        if (tz.useDaylightTime()) {
238            sb.append(tz.getDisplayName(true, TimeZone.LONG));
239            sb.append(SEPARATOR);
240            sb.append(tz.getDisplayName(true, TimeZone.SHORT));
241        } else {
242            sb.append(SEPARATOR);
243        }
244        sb.append(SEPARATOR);
245        sb.append(tz.getRawOffset() / 3600000f);
246        sb.append(SEPARATOR);
247        sb.append(tz.getDSTSavings() / 3600000f);
248        sb.append(SEPARATOR);
249        sb.append(country);
250        sb.append(SEPARATOR);
251
252        // 1-1-2013 noon GMT
253        sb.append(getLocalTime(1357041600000L));
254        sb.append(SEPARATOR);
255
256        // 3-15-2013 noon GMT
257        sb.append(getLocalTime(1363348800000L));
258        sb.append(SEPARATOR);
259
260        // 7-1-2013 noon GMT
261        sb.append(getLocalTime(1372680000000L));
262        sb.append(SEPARATOR);
263
264        // 11-01-2013 noon GMT
265        sb.append(getLocalTime(1383307200000L));
266        sb.append(SEPARATOR);
267
268        // if (this.mTransitions != null && this.mTransitions.length != 0) {
269        // sb.append('"');
270        // DateFormat df = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss Z",
271        // Locale.US);
272        // df.setTimeZone(tz);
273        // DateFormat weekdayFormat = new SimpleDateFormat("EEEE", Locale.US);
274        // weekdayFormat.setTimeZone(tz);
275        // Formatter f = new Formatter(sb);
276        // for (int i = 0; i < this.mTransitions.length; ++i) {
277        // if (this.mTransitions[i] < time) {
278        // continue;
279        // }
280        //
281        // String fromTime = formatTime(df, this.mTransitions[i] - 1);
282        // String toTime = formatTime(df, this.mTransitions[i]);
283        // f.format("%s -> %s (%d)", fromTime, toTime, this.mTransitions[i]);
284        //
285        // String weekday = weekdayFormat.format(new Date(1000L *
286        // this.mTransitions[i]));
287        // if (!weekday.equals("Sunday")) {
288        // f.format(" -- %s", weekday);
289        // }
290        // sb.append("##");
291        // }
292        // sb.append('"');
293        // }
294        // sb.append(SEPARATOR);
295        sb.append('\n');
296        return sb.toString();
297    }
298
299    private static String formatTime(DateFormat df, int s) {
300        long ms = s * 1000L;
301        return df.format(new Date(ms));
302    }
303
304    /*
305     * Returns a negative integer if this instance is less than the other; a
306     * positive integer if this instance is greater than the other; 0 if this
307     * instance has the same order as the other.
308     */
309    @Override
310    public int compareTo(TimeZoneInfo other) {
311
312        // TODO !!! Should compare the clock time instead of raw offset
313
314        // Higher raw offset comes before i.e. if the offset is bigger, return
315        // positive number.
316        if (this.mRawoffset != other.mRawoffset) {
317            return other.mRawoffset - this.mRawoffset;
318        }
319
320        // TZ with DST comes first because the offset is bigger during DST
321        // compared to a tz without DST
322        if (this.hasDst != other.hasDst) {
323            return this.hasDst ? -1 : 1;
324        }
325
326        // By country
327        if (this.mCountry == null) {
328            if (other.mCountry != null) {
329                return 1;
330            }
331        }
332
333        if (other.mCountry == null) {
334            return -1;
335        } else {
336            int diff = this.mCountry.compareTo(other.mCountry);
337
338            if (diff != 0) {
339                return diff;
340            }
341        }
342
343        if (Arrays.equals(this.mTransitions, other.mTransitions)) {
344            Log.e(TAG, "Not expected to be comparing tz with the same country, same offset," +
345                    " same dst, same transitions:\n" + this.toString() + "\n" + other.toString());
346        }
347
348        // Finally diff by display name
349        return this.mTz.getDisplayName(Locale.getDefault()).compareTo(
350                other.mTz.getDisplayName(Locale.getDefault()));
351    }
352}
353