2 * System.TimeZoneInfo Android Support
5 * Jonathan Pryor <jpryor@novell.com>
6 * The Android Open Source Project
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
24 using System
.Collections
.Generic
;
26 using System
.Runtime
.CompilerServices
;
27 using System
.Runtime
.InteropServices
;
32 interface IAndroidTimeZoneDB
{
33 IEnumerable
<string> GetAvailableIds ();
34 byte[] GetTimeZoneData (string id
);
37 [StructLayout (LayoutKind
.Sequential
, Pack
=1)]
38 unsafe struct AndroidTzDataHeader
{
39 public fixed byte signature
[12];
40 public int indexOffset
;
41 public int dataOffset
;
42 public int zoneTabOffset
;
45 [StructLayout (LayoutKind
.Sequential
, Pack
=1)]
46 unsafe struct AndroidTzDataEntry
{
47 public fixed byte id
[40];
48 public int byteOffset
;
50 public int rawUtcOffset
;
54 * Android v4.3 Timezone support infrastructure.
56 * This is a C# port of libcore.util.ZoneInfoDB:
58 * https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/ZoneInfoDB.java
60 * This is needed in order to read Android v4.3 tzdata files.
62 sealed class AndroidTzData
: IAndroidTimeZoneDB
{
64 internal static readonly string[] Paths
= new string[]{
65 Environment
.GetEnvironmentVariable ("ANDROID_DATA") + "/misc/zoneinfo/tzdata",
66 Environment
.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata",
78 public AndroidTzData (params string[] paths
)
80 foreach (var path
in paths
)
81 if (LoadData (path
)) {
86 Console
.Error
.WriteLine ("Couldn't find any tzdata!");
89 zoneTab
= "# Emergency fallback data.\n";
93 public string Version
{
97 public string ZoneTab
{
101 bool LoadData (string path
)
103 if (!File
.Exists (path
))
106 data
= File
.OpenRead (path
);
107 } catch (IOException
) {
109 } catch (UnauthorizedAccessException
) {
116 } catch (Exception e
) {
117 Console
.Error
.WriteLine ("tzdata file \"{0}\" was present but invalid: {1}", path
, e
);
122 unsafe void ReadHeader ()
124 int size
= System
.Math
.Max (Marshal
.SizeOf (typeof (AndroidTzDataHeader
)), Marshal
.SizeOf (typeof (AndroidTzDataEntry
)));
125 var buffer
= new byte [size
];
126 var header
= ReadAt
<AndroidTzDataHeader
>(0, buffer
);
128 header
.indexOffset
= NetworkToHostOrder (header
.indexOffset
);
129 header
.dataOffset
= NetworkToHostOrder (header
.dataOffset
);
130 header
.zoneTabOffset
= NetworkToHostOrder (header
.zoneTabOffset
);
132 sbyte* s
= (sbyte*) header
.signature
;
133 string magic
= new string (s
, 0, 6, Encoding
.ASCII
);
134 if (magic
!= "tzdata" || header
.signature
[11] != 0) {
135 var b
= new StringBuilder ();
136 b
.Append ("bad tzdata magic:");
137 for (int i
= 0; i
< 12; ++i
) {
138 b
.Append (" ").Append (((byte) s
[i
]).ToString ("x2"));
140 throw new InvalidOperationException ("bad tzdata magic: " + b
.ToString ());
143 version
= new string (s
, 6, 5, Encoding
.ASCII
);
145 ReadIndex (header
.indexOffset
, header
.dataOffset
, buffer
);
146 ReadZoneTab (header
.zoneTabOffset
, checked ((int) data
.Length
) - header
.zoneTabOffset
);
149 unsafe T ReadAt
<T
> (long position
, byte[] buffer
)
152 int size
= Marshal
.SizeOf (typeof (T
));
153 if (buffer
.Length
< size
)
154 throw new InvalidOperationException ("Internal error: buffer too small");
156 data
.Position
= position
;
158 if ((r
= data
.Read (buffer
, 0, size
)) < size
)
159 throw new InvalidOperationException (
160 string.Format ("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath
, r
, size
));
162 fixed (byte* b
= buffer
)
163 return (T
) Marshal
.PtrToStructure ((IntPtr
) b
, typeof (T
));
166 static int NetworkToHostOrder (int value)
168 if (!BitConverter
.IsLittleEndian
)
172 (((value >> 24) & 0xFF) |
173 ((value >> 08) & 0xFF00) |
174 ((value << 08) & 0xFF0000) |
178 unsafe void ReadIndex (int indexOffset
, int dataOffset
, byte[] buffer
)
180 int indexSize
= dataOffset
- indexOffset
;
181 int entryCount
= indexSize
/ Marshal
.SizeOf (typeof (AndroidTzDataEntry
));
182 int entrySize
= Marshal
.SizeOf (typeof (AndroidTzDataEntry
));
184 byteOffsets
= new int [entryCount
];
185 ids
= new string [entryCount
];
186 lengths
= new int [entryCount
];
188 for (int i
= 0; i
< entryCount
; ++i
) {
189 var entry
= ReadAt
<AndroidTzDataEntry
>(indexOffset
+ (entrySize
*i
), buffer
);
190 var p
= (sbyte*) entry
.id
;
192 byteOffsets
[i
] = NetworkToHostOrder (entry
.byteOffset
) + dataOffset
;
193 ids
[i
] = new string (p
, 0, GetStringLength (p
, 40), Encoding
.ASCII
);
194 lengths
[i
] = NetworkToHostOrder (entry
.length
);
196 if (lengths
[i
] < Marshal
.SizeOf (typeof (AndroidTzDataHeader
)))
197 throw new InvalidOperationException ("Length in index file < sizeof(tzhead)");
201 static unsafe int GetStringLength (sbyte* s
, int maxLength
)
204 for (len
= 0; len
< maxLength
; len
++, s
++) {
211 unsafe void ReadZoneTab (int zoneTabOffset
, int zoneTabSize
)
213 byte[] zoneTab
= new byte [zoneTabSize
];
215 data
.Position
= zoneTabOffset
;
218 if ((r
= data
.Read (zoneTab
, 0, zoneTab
.Length
)) < zoneTab
.Length
)
219 throw new InvalidOperationException (
220 string.Format ("Error reading zonetab: read {0} bytes, expected {1}", r
, zoneTabSize
));
222 this.zoneTab
= Encoding
.ASCII
.GetString (zoneTab
, 0, zoneTab
.Length
);
225 public IEnumerable
<string> GetAvailableIds ()
230 public byte[] GetTimeZoneData (string id
)
232 int i
= Array
.BinarySearch (ids
, id
, StringComparer
.Ordinal
);
236 int offset
= byteOffsets
[i
];
237 int length
= lengths
[i
];
238 var buffer
= new byte [length
];
241 data
.Position
= offset
;
243 if ((r
= data
.Read (buffer
, 0, buffer
.Length
)) < buffer
.Length
)
244 throw new InvalidOperationException (
245 string.Format ("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.",
246 tzdataPath
, offset
, length
, r
, buffer
.Length
));
249 TimeZoneInfo
.DumpTimeZoneDataToFile (id
, buffer
);
254 partial class TimeZoneInfo
{
256 static TimeZoneInfo
CreateLocal ()
258 return AndroidTimeZones
.Local
;
261 static TimeZoneInfo
FindSystemTimeZoneByIdCore (string id
)
263 var timeZoneInfo
= AndroidTimeZones
.GetTimeZone (id
, id
);
264 if (timeZoneInfo
== null)
265 throw new TimeZoneNotFoundException ();
269 static void GetSystemTimeZonesCore (List
<TimeZoneInfo
> systemTimeZones
)
271 foreach (string id
in AndroidTimeZones
.GetAvailableIds ()) {
272 var tz
= AndroidTimeZones
.GetTimeZone (id
, id
);
274 systemTimeZones
.Add (tz
);
279 * Android < v4.3 Timezone support infrastructure.
281 * This is a C# port of org.apache.harmony.luni.internal.util.ZoneInfoDB:
283 * http://android.git.kernel.org/?p=platform/libcore.git;a=blob;f=luni/src/main/java/org/apache/harmony/luni/internal/util/ZoneInfoDB.java;h=3e7bdc3a952b24da535806d434a3a27690feae26;hb=HEAD
285 * From the ZoneInfoDB source:
287 * However, to conserve disk space the data for all time zones are
288 * concatenated into a single file, and a second file is used to indicate
289 * the starting position of each time zone record. A third file indicates
290 * the version of the zoneinfo databse used to generate the data.
292 * which succinctly describes why we can't just use the LIBC implementation in
293 * TimeZoneInfo.cs -- the "standard Unixy" directory structure is NOT used.
295 sealed class ZoneInfoDB
: IAndroidTimeZoneDB
{
296 const int TimeZoneNameLength
= 40;
297 const int TimeZoneIntSize
= 4;
299 internal static readonly string ZoneDirectoryName
= Environment
.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/";
301 const string ZoneFileName
= "zoneinfo.dat";
302 const string IndexFileName
= "zoneinfo.idx";
303 const string DefaultVersion
= "2007h";
304 const string VersionFileName
= "zoneinfo.version";
306 readonly string zoneRoot
;
307 readonly string version
;
308 readonly string[] names
;
309 readonly int[] starts
;
310 readonly int[] lengths
;
311 readonly int[] offsets
;
313 public ZoneInfoDB (string zoneInfoDB
= null)
315 zoneRoot
= zoneInfoDB
?? ZoneDirectoryName
;
317 version
= ReadVersion (Path
.Combine (zoneRoot
, VersionFileName
));
319 version
= DefaultVersion
;
323 ReadDatabase (Path
.Combine (zoneRoot
, IndexFileName
), out names
, out starts
, out lengths
, out offsets
);
325 names
= new string [0];
326 starts
= new int [0];
327 lengths
= new int [0];
328 offsets
= new int [0];
332 static string ReadVersion (string path
)
334 using (var file
= new StreamReader (path
, Encoding
.GetEncoding ("iso-8859-1"))) {
335 return file
.ReadToEnd ().Trim ();
339 void ReadDatabase (string path
, out string[] names
, out int[] starts
, out int[] lengths
, out int[] offsets
)
341 using (var file
= File
.OpenRead (path
)) {
342 var nbuf
= new byte [TimeZoneNameLength
];
344 int numEntries
= (int) (file
.Length
/ (TimeZoneNameLength
+ 3*TimeZoneIntSize
));
346 char[] namebuf
= new char [TimeZoneNameLength
];
348 names
= new string [numEntries
];
349 starts
= new int [numEntries
];
350 lengths
= new int [numEntries
];
351 offsets
= new int [numEntries
];
353 for (int i
= 0; i
< numEntries
; ++i
) {
354 Fill (file
, nbuf
, nbuf
.Length
);
356 for (namelen
= 0; namelen
< nbuf
.Length
; ++namelen
) {
357 if (nbuf
[namelen
] == '\0')
359 namebuf
[namelen
] = (char) (nbuf
[namelen
] & 0xFF);
362 names
[i
] = new string (namebuf
, 0, namelen
);
363 starts
[i
] = ReadInt32 (file
, nbuf
);
364 lengths
[i
] = ReadInt32 (file
, nbuf
);
365 offsets
[i
] = ReadInt32 (file
, nbuf
);
370 static void Fill (Stream stream
, byte[] nbuf
, int required
)
372 int read
= 0, offset
= 0;
373 while (offset
< required
&& (read
= stream
.Read (nbuf
, offset
, required
- offset
)) > 0)
375 if (read
!= required
)
376 throw new EndOfStreamException ("Needed to read " + required
+ " bytes; read " + read
+ " bytes");
379 // From java.io.RandomAccessFioe.readInt(), as we need to use the same
380 // byte ordering as Java uses.
381 static int ReadInt32 (Stream stream
, byte[] nbuf
)
383 Fill (stream
, nbuf
, 4);
384 return ((nbuf
[0] & 0xff) << 24) + ((nbuf
[1] & 0xff) << 16) +
385 ((nbuf
[2] & 0xff) << 8) + (nbuf
[3] & 0xff);
388 internal string Version
{
389 get {return version;}
392 public IEnumerable
<string> GetAvailableIds ()
394 return GetAvailableIds (0, false);
397 IEnumerable
<string> GetAvailableIds (int rawOffset
)
399 return GetAvailableIds (rawOffset
, true);
402 IEnumerable
<string> GetAvailableIds (int rawOffset
, bool checkOffset
)
404 for (int i
= 0; i
< offsets
.Length
; ++i
) {
405 if (!checkOffset
|| offsets
[i
] == rawOffset
)
406 yield return names
[i
];
410 public byte[] GetTimeZoneData (string id
)
413 using (var stream
= GetTimeZoneData (id
, out start
, out length
)) {
416 byte[] buf
= new byte [length
];
417 Fill (stream
, buf
, buf
.Length
);
422 FileStream
GetTimeZoneData (string name
, out int start
, out int length
)
424 if (name
== null) { // Just in case, to avoid NREX as in xambug #4902
430 var f
= new FileInfo (Path
.Combine (zoneRoot
, name
));
433 length
= (int) f
.Length
;
434 return f
.OpenRead ();
439 int i
= Array
.BinarySearch (names
, name
, StringComparer
.Ordinal
);
444 length
= lengths
[i
];
446 var stream
= File
.OpenRead (Path
.Combine (zoneRoot
, ZoneFileName
));
447 stream
.Seek (start
, SeekOrigin
.Begin
);
453 static class AndroidTimeZones
{
455 static IAndroidTimeZoneDB db
;
457 static AndroidTimeZones ()
459 db
= GetDefaultTimeZoneDB ();
462 static IAndroidTimeZoneDB
GetDefaultTimeZoneDB ()
464 foreach (var p
in AndroidTzData
.Paths
)
466 return new AndroidTzData (AndroidTzData
.Paths
);
467 if (Directory
.Exists (ZoneInfoDB
.ZoneDirectoryName
))
468 return new ZoneInfoDB ();
472 internal static IEnumerable
<string> GetAvailableIds ()
476 : db
.GetAvailableIds ();
479 static TimeZoneInfo
_GetTimeZone (string id
, string name
)
483 byte[] buffer
= db
.GetTimeZoneData (name
);
486 return TimeZoneInfo
.ParseTZBuffer (id
, buffer
, buffer
.Length
);
489 internal static TimeZoneInfo
GetTimeZone (string id
, string name
)
492 if (name
== "GMT" || name
== "UTC")
493 return new TimeZoneInfo (id
, TimeSpan
.FromSeconds (0), id
, name
, name
, null, disableDaylightSavingTime
:true);
494 if (name
.StartsWith ("GMT"))
495 return new TimeZoneInfo (id
,
496 TimeSpan
.FromSeconds (ParseNumericZone (name
)),
497 id
, name
, name
, null, disableDaylightSavingTime
:true);
501 return _GetTimeZone (id
, name
);
502 } catch (Exception
) {
507 static int ParseNumericZone (string name
)
509 if (name
== null || !name
.StartsWith ("GMT") || name
.Length
<= 3)
515 else if (name
[3] == '-')
523 for (where
= 4; where
< name
.Length
; where
++) {
524 char c
= name
[where
];
532 if (c
>= '0' && c
<= '9')
533 hour
= hour
* 10 + c
- '0';
539 for (; where
< name
.Length
; where
++) {
540 char c
= name
[where
];
542 if (c
>= '0' && c
<= '9')
543 min
= min
* 10 + c
- '0';
549 return sign
* (hour
* 60 + min
) * 60;
550 else if (hour
>= 100)
551 return sign
* ((hour
/ 100) * 60 + (hour
% 100)) * 60;
553 return sign
* (hour
* 60) * 60;
556 internal static TimeZoneInfo Local
{
558 var id
= GetDefaultTimeZoneName ();
559 return GetTimeZone (id
, id
);
563 [DllImport ("__Internal")]
564 static extern int monodroid_get_system_property (string name
, ref IntPtr
value);
566 [DllImport ("__Internal")]
567 static extern void monodroid_free (IntPtr ptr
);
569 static string GetDefaultTimeZoneName ()
571 IntPtr
value = IntPtr
.Zero
;
573 string defaultTimeZone
= Environment
.GetEnvironmentVariable ("__XA_OVERRIDE_TIMEZONE_ID__");
575 if (!string.IsNullOrEmpty (defaultTimeZone
))
576 return defaultTimeZone
;
579 if (Environment
.GetEnvironmentVariable ("__XA_USE_JAVA_DEFAULT_TIMEZONE_ID__") == null)
580 n
= monodroid_get_system_property ("persist.sys.timezone", ref value);
582 if (n
> 0 && value != IntPtr
.Zero
) {
583 defaultTimeZone
= (Marshal
.PtrToStringAnsi (value) ?? String
.Empty
).Trim ();
584 monodroid_free (value);
585 if (!String
.IsNullOrEmpty (defaultTimeZone
))
586 return defaultTimeZone
;
589 defaultTimeZone
= (AndroidPlatform
.GetDefaultTimeZone () ?? String
.Empty
).Trim ();
590 if (!String
.IsNullOrEmpty (defaultTimeZone
))
591 return defaultTimeZone
;
599 * mcs /debug+ /out:tzi.exe /unsafe "/d:INSIDE_CORLIB;MONODROID;NET_4_0;LIBC;SELF_TEST" ../corlib/System/AndroidPlatform.cs System/TimeZone*.cs ../../build/common/Consts.cs ../Mono.Options/Mono.Options/Options.cs
601 * mkdir -p android/tzdb/usr/share/zoneinfo
602 * mkdir -p android/tzdb/misc/zoneinfo/zoneinfo
603 * android_root=`adb shell echo '$ANDROID_ROOT' | tr -d "\r"`
604 * android_data=`adb shell echo '$ANDROID_DATA' | tr -d "\r"`
605 * adb pull $android_root/usr/share/zoneinfo android/tzdb/usr/share/zoneinfo
606 * adb pull $android_data/misc/zoneinfo/tzdata android/tzdb/misc/zoneinfo
608 * # Dump all timezone names
609 * __XA_OVERRIDE_TIMEZONE_ID__=America/New_York ANDROID_ROOT=`pwd` ANDROID_DATA=`pwd` mono --debug tzi.exe --offset=1969-01-01
611 * # Dump TimeZone data to files under path `tzdata`
612 * __XA_OVERRIDE_TIMEZONE_ID__=America/New_York ANDROID_ROOT=`pwd` ANDROID_DATA=`pwd` mono --debug tzi.exe -o android/tzdata
614 * # Dump TimeZone rules for specific timezone data (as dumped above)
615 * mono tzi.exe --offset=2012-10-24 -i=tzdata/Asia/Amman
617 static void Main (string[] args
)
619 DateTime
? offset
= null;
620 Func
<IAndroidTimeZoneDB
> c
= () => GetDefaultTimeZoneDB ();
621 bool dump_rules
= false;
622 Mono
.Options
.OptionSet p
= null;
623 p
= new Mono
.Options
.OptionSet () {
625 "TimeZone data {FILE} to parse and dump",
626 v
=> DumpTimeZoneFile (v
, offset
)
629 "Write TimeZone data files to {PATH}",
630 v
=> TimeZoneInfo
.TimeZoneDataExportPath
= v
632 { "T=", "Create AndroidTzData from {PATH}
.", v => {
633 c = () => new AndroidTzData (v);
635 { "Z=", "Create ZoneInfoDB from {DIR}.", v
=> {
636 c
= () => new ZoneInfoDB (v
);
638 { "offset=", "Show timezone info offset for DateTime {OFFSET}
.", v => {
639 offset = DateTime.Parse (v);
640 Console.WriteLine ("Using DateTime Offset
: {0}
", offset);
643 "Show timezone info offset
for DateTime {OFFSET}
.",
644 v => dump_rules = v != null },
645 { "help
", "Show
this message and exit
", v => {
646 p.WriteOptionDescriptions (Console.Out);
647 Environment.Exit (0);
651 AndroidTimeZones.db = c ();
652 Console.WriteLine ("DB type
: {0}
", AndroidTimeZones.db.GetType ().FullName);
653 foreach (var id in GetAvailableIds ()) {
654 Console.Write ("name
={0,-40}
", id);
656 TimeZoneInfo zone = _GetTimeZone (id, id);
658 Console.Write (" {0,-40}
", zone);
659 if (offset.HasValue) {
660 Console.Write ("From Offset
: {0}
", zone.GetUtcOffset (offset.Value));
663 WriteZoneRules (zone);
667 Console.Write (" ERROR
:null");
669 } catch (Exception e) {
670 Console.WriteLine ();
671 Console.Write ("ERROR
: {0}
", e);
673 Console.WriteLine ();
677 static void WriteZoneRules (TimeZoneInfo zone)
679 var rules = zone.GetAdjustmentRules ();
680 for (int i = 0; i < rules.Length; ++i) {
681 var rule = rules [i];
682 Console.WriteLine ();
683 Console.Write ("\tAdjustmentRules
[{0,3}
]: DaylightDelta
={1}
; DateStart
={2:yyyy-MM}
; DateEnd
={3:yyyy-MM}
; DaylightTransitionStart
={4:D2}
-{5:D2}T{6}
; DaylightTransitionEnd
={7:D2}
-{8:D2}T{9}
",
686 rule.DateStart, rule.DateEnd,
687 rule.DaylightTransitionStart.Month, rule.DaylightTransitionStart.Day, rule.DaylightTransitionStart.TimeOfDay.TimeOfDay,
688 rule.DaylightTransitionEnd.Month, rule.DaylightTransitionEnd.Day, rule.DaylightTransitionEnd.TimeOfDay.TimeOfDay);
692 static void DumpTimeZoneFile (string path, DateTime? time)
694 var buffer = File.ReadAllBytes (path);
695 var zone = ParseTZBuffer (path, buffer, buffer.Length);
696 Console.Write ("Rules
for: {0}
", path);
697 WriteZoneRules (zone);
698 Console.WriteLine ();
700 var offset = zone.GetUtcOffset (time.Value);
701 var isDst = zone.IsDaylightSavingTime (time.Value);
702 Console.WriteLine ("\tDate
({0}
): Offset({1}
) IsDST({2}
)", time.Value, offset, isDst);
705 if (zone.transitions != null) {
706 Console.WriteLine ("Transitions
for: {0}
", path);
707 foreach (var transition in zone.transitions) {
708 Console.WriteLine ("\t Date({0}
): {1}
", transition.Key, transition.Value);
716 static string TimeZoneDataExportPath;
719 internal static void DumpTimeZoneDataToFile (string id, byte[] buffer)
722 int p = id.LastIndexOf ('/');
723 var o = Path.Combine (TimeZoneDataExportPath,
724 p >= 0 ? id.Substring (0, p) : id);
726 o = Path.Combine (o, id.Substring (p+1));
727 Directory.CreateDirectory (Path.GetDirectoryName (o));
728 using (var f = File.OpenWrite (o))
729 f.Write (buffer, 0, buffer.Length);