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