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