1package com.xtremelabs.robolectric.shadows; 2 3 4import android.text.format.Time; 5import android.util.TimeFormatException; 6import com.xtremelabs.robolectric.internal.Implementation; 7import com.xtremelabs.robolectric.internal.Implements; 8import com.xtremelabs.robolectric.internal.RealObject; 9 10import java.lang.reflect.Constructor; 11import java.lang.reflect.InvocationTargetException; 12import java.text.ParseException; 13import java.text.SimpleDateFormat; 14import java.util.*; 15 16@Implements(Time.class) 17public class ShadowTime { 18 @RealObject 19 private Time time; 20 21 private static final long SECOND_IN_MILLIS = 1000; 22 private static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60; 23 private static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60; 24 private static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24; 25 26 public void __constructor__() { 27 __constructor__(getCurrentTimezone()); 28 } 29 30 public void __constructor__(String timezone) { 31 if (timezone == null) { 32 throw new NullPointerException("timezone is null!"); 33 } 34 time.timezone = timezone; 35 time.year = 1970; 36 time.monthDay = 1; 37 time.isDst = -1; 38 } 39 40 public void __constructor__(Time other) { 41 set(other); 42 } 43 44 @Implementation 45 public void set(Time other) { 46 time.timezone = other.timezone; 47 time.second = other.second; 48 time.minute = other.minute; 49 time.hour = other.hour; 50 time.monthDay = other.monthDay; 51 time.month = other.month; 52 time.year = other.year; 53 time.weekDay = other.weekDay; 54 time.yearDay = other.yearDay; 55 time.isDst = other.isDst; 56 time.gmtoff = other.gmtoff; 57 } 58 59 @Implementation 60 public void setToNow() { 61 set(System.currentTimeMillis()); 62 } 63 64 65 @Implementation 66 public static boolean isEpoch(Time time) { 67 long millis = time.toMillis(true); 68 return getJulianDay(millis, 0) == Time.EPOCH_JULIAN_DAY; 69 } 70 71 72 @Implementation 73 public static int getJulianDay(long millis, long gmtoff) { 74 long offsetMillis = gmtoff * 1000; 75 long julianDay = (millis + offsetMillis) / DAY_IN_MILLIS; 76 return (int) julianDay + Time.EPOCH_JULIAN_DAY; 77 } 78 79 @Implementation 80 public long setJulianDay(int julianDay) { 81 // Don't bother with the GMT offset since we don't know the correct 82 // value for the given Julian day. Just get close and then adjust 83 // the day. 84 //long millis = (julianDay - EPOCH_JULIAN_DAY) * DateUtils.DAY_IN_MILLIS; 85 long millis = (julianDay - Time.EPOCH_JULIAN_DAY) * DAY_IN_MILLIS; 86 set(millis); 87 88 // Figure out how close we are to the requested Julian day. 89 // We can't be off by more than a day. 90 int approximateDay = getJulianDay(millis, time.gmtoff); 91 int diff = julianDay - approximateDay; 92 time.monthDay += diff; 93 94 // Set the time to 12am and re-normalize. 95 time.hour = 0; 96 time.minute = 0; 97 time.second = 0; 98 millis = time.normalize(true); 99 return millis; 100 } 101 102 @Implementation 103 public void set(long millis) { 104 Calendar c = getCalendar(); 105 c.setTimeInMillis(millis); 106 set( 107 c.get(Calendar.SECOND), 108 c.get(Calendar.MINUTE), 109 c.get(Calendar.HOUR_OF_DAY), 110 c.get(Calendar.DAY_OF_MONTH), 111 c.get(Calendar.MONTH), 112 c.get(Calendar.YEAR) 113 ); 114 } 115 116 @Implementation 117 public long toMillis(boolean ignoreDst) { 118 Calendar c = getCalendar(); 119 return c.getTimeInMillis(); 120 } 121 122 @Implementation 123 public void set(int second, int minute, int hour, int monthDay, int month, int year) { 124 time.second = second; 125 time.minute = minute; 126 time.hour = hour; 127 time.monthDay = monthDay; 128 time.month = month; 129 time.year = year; 130 time.weekDay = 0; 131 time.yearDay = 0; 132 time.isDst = -1; 133 time.gmtoff = 0; 134 } 135 136 @Implementation 137 public void set(int monthDay, int month, int year) { 138 set(0, 0, 0, monthDay, month, year); 139 } 140 141 @Implementation 142 public void clear(String timezone) { 143 if (timezone == null) { 144 throw new NullPointerException("timezone is null!"); 145 } 146 time.timezone = timezone; 147 time.allDay = false; 148 time.second = 0; 149 time.minute = 0; 150 time.hour = 0; 151 time.monthDay = 0; 152 time.month = 0; 153 time.year = 0; 154 time.weekDay = 0; 155 time.yearDay = 0; 156 time.gmtoff = 0; 157 time.isDst = -1; 158 } 159 160 @Implementation 161 public static String getCurrentTimezone() { 162 return TimeZone.getDefault().getID(); 163 } 164 165 @Implementation 166 public static int compare(Time a, Time b) { 167 long ams = a.toMillis(false); 168 long bms = b.toMillis(false); 169 if (ams == bms) { 170 return 0; 171 } else if (ams < bms) { 172 return -1; 173 } else { 174 return 1; 175 } 176 } 177 178 @Implementation 179 public boolean before(Time other) { 180 return Time.compare(time, other) < 0; 181 } 182 183 @Implementation 184 public boolean after(Time other) { 185 return Time.compare(time, other) > 0; 186 } 187 188 @Implementation 189 public boolean parse(String timeString) { 190 TimeZone tz; 191 if (timeString.endsWith("Z")) { 192 timeString = timeString.substring(0, timeString.length() - 1); 193 tz = TimeZone.getTimeZone("UTC"); 194 } else { 195 tz = TimeZone.getTimeZone(time.timezone); 196 } 197 SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH); 198 SimpleDateFormat dfShort = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH); 199 df.setTimeZone(tz); 200 dfShort.setTimeZone(tz); 201 time.timezone = tz.getID(); 202 try { 203 set(df.parse(timeString).getTime()); 204 } catch (ParseException e) { 205 try { 206 set(dfShort.parse(timeString).getTime()); 207 } catch (ParseException e2) { 208 throwTimeFormatException(); 209 } 210 } 211 return "UTC".equals(tz.getID()); 212 } 213 214 @Implementation 215 public String format(String format) { 216 Strftime strftime = new Strftime(format, Locale.getDefault()); 217 strftime.setTimeZone(TimeZone.getTimeZone(time.timezone)); 218 return strftime.format(new Date(toMillis(false))); 219 } 220 221 @Implementation 222 public String format2445() { 223 return format("%Y%m%dT%H%M%S"); 224 } 225 226 @Implementation 227 public String format3339(boolean allDay) { 228 if (allDay) { 229 return format("%Y-%m-%d"); 230 } else if ("UTC".equals(time.timezone)) { 231 return format("%Y-%m-%dT%H:%M:%S.000Z"); 232 } else { 233 String base = format("%Y-%m-%dT%H:%M:%S.000"); 234 String sign = (time.gmtoff < 0) ? "-" : "+"; 235 int offset = (int) Math.abs(time.gmtoff); 236 int minutes = (offset % 3600) / 60; 237 int hours = offset / 3600; 238 return String.format("%s%s%02d:%02d", base, sign, hours, minutes); 239 } 240 } 241 242 private void throwTimeFormatException() { 243 try { 244 Constructor<TimeFormatException> c = TimeFormatException.class.getDeclaredConstructor(); 245 c.setAccessible(true); 246 throw c.newInstance(); 247 } catch (InvocationTargetException e) { 248 throw new RuntimeException(e); 249 } catch (InstantiationException e) { 250 throw new RuntimeException(e); 251 } catch (IllegalAccessException e) { 252 throw new RuntimeException(e); 253 } catch (NoSuchMethodException e) { 254 throw new RuntimeException(e); 255 } 256 } 257 258 private Calendar getCalendar() { 259 Calendar c = Calendar.getInstance(TimeZone.getTimeZone(time.timezone)); 260 c.set(time.year, time.month, time.monthDay, time.hour, time.minute, time.second); 261 c.set(Calendar.MILLISECOND, 0); 262 return c; 263 } 264 265 // taken from org.apache.catalina.util.Strftime.java 266 // see http://javasourcecode.org/html/open-source/tomcat/tomcat-6.0.32/org/apache/catalina/util/Strftime.java.html 267 /* 268 * Licensed to the Apache Software Foundation (ASF) under one or more 269 * contributor license agreements. See the NOTICE file distributed with 270 * this work for additional information regarding copyright ownership. 271 * The ASF licenses this file to You under the Apache License, Version 2.0 272 * (the "License"); you may not use this file except in compliance with 273 * the License. You may obtain a copy of the License at 274 * 275 * http://www.apache.org/licenses/LICENSE-2.0 276 * 277 * Unless required by applicable law or agreed to in writing, software 278 * distributed under the License is distributed on an "AS IS" BASIS, 279 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 280 * See the License for the specific language governing permissions and 281 * limitations under the License. 282 */ 283 public static class Strftime { 284 protected static Properties translate; 285 protected SimpleDateFormat simpleDateFormat; 286 287 /** 288 * Initialize our pattern translation 289 */ 290 static { 291 translate = new Properties(); 292 translate.put("a", "EEE"); 293 translate.put("A", "EEEE"); 294 translate.put("b", "MMM"); 295 translate.put("B", "MMMM"); 296 translate.put("c", "EEE MMM d HH:mm:ss yyyy"); 297 298 //There's no way to specify the century in SimpleDateFormat. We don't want to hard-code 299 //20 since this could be wrong for the pre-2000 files. 300 //translate.put("C", "20"); 301 translate.put("d", "dd"); 302 translate.put("D", "MM/dd/yy"); 303 translate.put("e", "dd"); //will show as '03' instead of ' 3' 304 translate.put("F", "yyyy-MM-dd"); 305 translate.put("g", "yy"); 306 translate.put("G", "yyyy"); 307 translate.put("H", "HH"); 308 translate.put("h", "MMM"); 309 translate.put("I", "hh"); 310 translate.put("j", "DDD"); 311 translate.put("k", "HH"); //will show as '07' instead of ' 7' 312 translate.put("l", "hh"); //will show as '07' instead of ' 7' 313 translate.put("m", "MM"); 314 translate.put("M", "mm"); 315 translate.put("n", "\n"); 316 translate.put("p", "a"); 317 translate.put("P", "a"); //will show as pm instead of PM 318 translate.put("r", "hh:mm:ss a"); 319 translate.put("R", "HH:mm"); 320 //There's no way to specify this with SimpleDateFormat 321 //translate.put("s","seconds since ecpoch"); 322 translate.put("S", "ss"); 323 translate.put("t", "\t"); 324 translate.put("T", "HH:mm:ss"); 325 //There's no way to specify this with SimpleDateFormat 326 //translate.put("u","day of week ( 1-7 )"); 327 328 //There's no way to specify this with SimpleDateFormat 329 //translate.put("U","week in year with first sunday as first day..."); 330 331 translate.put("V", "ww"); //I'm not sure this is always exactly the same 332 333 //There's no way to specify this with SimpleDateFormat 334 //translate.put("W","week in year with first monday as first day..."); 335 336 //There's no way to specify this with SimpleDateFormat 337 //translate.put("w","E"); 338 translate.put("X", "HH:mm:ss"); 339 translate.put("x", "MM/dd/yy"); 340 translate.put("y", "yy"); 341 translate.put("Y", "yyyy"); 342 translate.put("Z", "z"); 343 translate.put("z", "Z"); 344 translate.put("%", "%"); 345 } 346 347 348 /** 349 * Create an instance of this date formatting class 350 * 351 * @see #Strftime(String, Locale) 352 */ 353 public Strftime(String origFormat) { 354 String convertedFormat = convertDateFormat(origFormat); 355 simpleDateFormat = new SimpleDateFormat(convertedFormat); 356 } 357 358 /** 359 * Create an instance of this date formatting class 360 * 361 * @param origFormat the strftime-style formatting string 362 * @param locale the locale to use for locale-specific conversions 363 */ 364 public Strftime(String origFormat, Locale locale) { 365 String convertedFormat = convertDateFormat(origFormat); 366 simpleDateFormat = new SimpleDateFormat(convertedFormat, locale); 367 } 368 369 /** 370 * Format the date according to the strftime-style string given in the constructor. 371 * 372 * @param date the date to format 373 * @return the formatted date 374 */ 375 public String format(Date date) { 376 return simpleDateFormat.format(date); 377 } 378 379 /** 380 * Get the timezone used for formatting conversions 381 * 382 * @return the timezone 383 */ 384 public TimeZone getTimeZone() { 385 return simpleDateFormat.getTimeZone(); 386 } 387 388 /** 389 * Change the timezone used to format dates 390 * 391 * @see SimpleDateFormat#setTimeZone 392 */ 393 public void setTimeZone(TimeZone timeZone) { 394 simpleDateFormat.setTimeZone(timeZone); 395 } 396 397 /** 398 * Search the provided pattern and get the C standard 399 * Date/Time formatting rules and convert them to the 400 * Java equivalent. 401 * 402 * @param pattern The pattern to search 403 * @return The modified pattern 404 */ 405 protected String convertDateFormat(String pattern) { 406 boolean inside = false; 407 boolean mark = false; 408 boolean modifiedCommand = false; 409 410 StringBuffer buf = new StringBuffer(); 411 412 for (int i = 0; i < pattern.length(); i++) { 413 char c = pattern.charAt(i); 414 415 if (c == '%' && !mark) { 416 mark = true; 417 } else { 418 if (mark) { 419 if (modifiedCommand) { 420 //don't do anything--we just wanted to skip a char 421 modifiedCommand = false; 422 mark = false; 423 } else { 424 inside = translateCommand(buf, pattern, i, inside); 425 //It's a modifier code 426 if (c == 'O' || c == 'E') { 427 modifiedCommand = true; 428 } else { 429 mark = false; 430 } 431 } 432 } else { 433 if (!inside && c != ' ') { 434 //We start a literal, which we need to quote 435 buf.append("'"); 436 inside = true; 437 } 438 439 buf.append(c); 440 } 441 } 442 } 443 444 if (buf.length() > 0) { 445 char lastChar = buf.charAt(buf.length() - 1); 446 447 if (lastChar != '\'' && inside) { 448 buf.append('\''); 449 } 450 } 451 return buf.toString(); 452 } 453 454 protected String quote(String str, boolean insideQuotes) { 455 String retVal = str; 456 if (!insideQuotes) { 457 retVal = '\'' + retVal + '\''; 458 } 459 return retVal; 460 } 461 462 /** 463 * Try to get the Java Date/Time formatting associated with 464 * the C standard provided. 465 * 466 * @param buf The buffer 467 * @param pattern The date/time pattern 468 * @param index The char index 469 * @param oldInside Flag value 470 * @return True if new is inside buffer 471 */ 472 protected boolean translateCommand(StringBuffer buf, String pattern, int index, boolean oldInside) { 473 char firstChar = pattern.charAt(index); 474 boolean newInside = oldInside; 475 476 //O and E are modifiers, they mean to present an alternative representation of the next char 477 //we just handle the next char as if the O or E wasn't there 478 if (firstChar == 'O' || firstChar == 'E') { 479 if (index + 1 < pattern.length()) { 480 newInside = translateCommand(buf, pattern, index + 1, oldInside); 481 } else { 482 buf.append(quote("%" + firstChar, oldInside)); 483 } 484 } else { 485 String command = translate.getProperty(String.valueOf(firstChar)); 486 487 //If we don't find a format, treat it as a literal--That's what apache does 488 if (command == null) { 489 buf.append(quote("%" + firstChar, oldInside)); 490 } else { 491 //If we were inside quotes, close the quotes 492 if (oldInside) { 493 buf.append('\''); 494 } 495 buf.append(command); 496 newInside = false; 497 } 498 } 499 return newInside; 500 } 501 } 502} 503