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