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.
21 #if (INSIDE_CORLIB && MONODROID)
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));
253 partial class TimeZoneInfo {
256 * Android < v4.3 Timezone support infrastructure.
258 * This is a C# port of org.apache.harmony.luni.internal.util.ZoneInfoDB:
260 * 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
262 * From the ZoneInfoDB source:
264 * However, to conserve disk space the data for all time zones are
265 * concatenated into a single file, and a second file is used to indicate
266 * the starting position of each time zone record. A third file indicates
267 * the version of the zoneinfo databse used to generate the data.
269 * which succinctly describes why we can't just use the LIBC implementation in
270 * TimeZoneInfo.cs -- the "standard Unixy" directory structure is NOT used.
272 sealed class ZoneInfoDB : IAndroidTimeZoneDB {
273 const int TimeZoneNameLength = 40;
274 const int TimeZoneIntSize = 4;
276 internal static readonly string ZoneDirectoryName = Environment.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/";
278 const string ZoneFileName = "zoneinfo.dat";
279 const string IndexFileName = "zoneinfo.idx";
280 const string DefaultVersion = "2007h";
281 const string VersionFileName = "zoneinfo.version";
283 readonly string zoneRoot;
284 readonly string version;
285 readonly string[] names;
286 readonly int[] starts;
287 readonly int[] lengths;
288 readonly int[] offsets;
290 public ZoneInfoDB (string zoneInfoDB = null)
292 zoneRoot = zoneInfoDB ?? ZoneDirectoryName;
294 version = ReadVersion (Path.Combine (zoneRoot, VersionFileName));
296 version = DefaultVersion;
300 ReadDatabase (Path.Combine (zoneRoot, IndexFileName), out names, out starts, out lengths, out offsets);
302 names = new string [0];
303 starts = new int [0];
304 lengths = new int [0];
305 offsets = new int [0];
309 static string ReadVersion (string path)
311 using (var file = new StreamReader (path, Encoding.GetEncoding ("iso-8859-1"))) {
312 return file.ReadToEnd ().Trim ();
316 void ReadDatabase (string path, out string[] names, out int[] starts, out int[] lengths, out int[] offsets)
318 using (var file = File.OpenRead (path)) {
319 var nbuf = new byte [TimeZoneNameLength];
321 int numEntries = (int) (file.Length / (TimeZoneNameLength + 3*TimeZoneIntSize));
323 char[] namebuf = new char [TimeZoneNameLength];
325 names = new string [numEntries];
326 starts = new int [numEntries];
327 lengths = new int [numEntries];
328 offsets = new int [numEntries];
330 for (int i = 0; i < numEntries; ++i) {
331 Fill (file, nbuf, nbuf.Length);
333 for (namelen = 0; namelen < nbuf.Length; ++namelen) {
334 if (nbuf [namelen] == '\0')
336 namebuf [namelen] = (char) (nbuf [namelen] & 0xFF);
339 names [i] = new string (namebuf, 0, namelen);
340 starts [i] = ReadInt32 (file, nbuf);
341 lengths [i] = ReadInt32 (file, nbuf);
342 offsets [i] = ReadInt32 (file, nbuf);
347 static void Fill (Stream stream, byte[] nbuf, int required)
349 int read, offset = 0;
350 while (offset < required && (read = stream.Read (nbuf, offset, required - offset)) > 0)
352 if (read != required)
353 throw new EndOfStreamException ("Needed to read " + required + " bytes; read " + read + " bytes");
356 // From java.io.RandomAccessFioe.readInt(), as we need to use the same
357 // byte ordering as Java uses.
358 static int ReadInt32 (Stream stream, byte[] nbuf)
360 Fill (stream, nbuf, 4);
361 return ((nbuf [0] & 0xff) << 24) + ((nbuf [1] & 0xff) << 16) +
362 ((nbuf [2] & 0xff) << 8) + (nbuf [3] & 0xff);
365 internal string Version {
366 get {return version;}
369 public IEnumerable<string> GetAvailableIds ()
371 return GetAvailableIds (0, false);
374 IEnumerable<string> GetAvailableIds (int rawOffset)
376 return GetAvailableIds (rawOffset, true);
379 IEnumerable<string> GetAvailableIds (int rawOffset, bool checkOffset)
381 for (int i = 0; i < offsets.Length; ++i) {
382 if (!checkOffset || offsets [i] == rawOffset)
383 yield return names [i];
387 public byte[] GetTimeZoneData (string id)
390 using (var stream = GetTimeZoneData (id, out start, out length)) {
393 byte[] buf = new byte [length];
394 Fill (stream, buf, buf.Length);
399 FileStream GetTimeZoneData (string name, out int start, out int length)
401 var f = new FileInfo (Path.Combine (zoneRoot, name));
404 length = (int) f.Length;
405 return f.OpenRead ();
410 int i = Array.BinarySearch (names, name, StringComparer.Ordinal);
415 length = lengths [i];
417 var stream = File.OpenRead (Path.Combine (zoneRoot, ZoneFileName));
418 stream.Seek (start, SeekOrigin.Begin);
424 static class AndroidTimeZones {
426 static IAndroidTimeZoneDB db;
428 static AndroidTimeZones ()
430 db = GetDefaultTimeZoneDB ();
433 static IAndroidTimeZoneDB GetDefaultTimeZoneDB ()
435 foreach (var p in AndroidTzData.Paths)
437 return new AndroidTzData (AndroidTzData.Paths);
438 if (Directory.Exists (ZoneInfoDB.ZoneDirectoryName))
439 return new ZoneInfoDB ();
443 internal static IEnumerable<string> GetAvailableIds ()
447 : db.GetAvailableIds ();
450 static TimeZoneInfo _GetTimeZone (string name)
454 byte[] buffer = db.GetTimeZoneData (name);
457 return TimeZoneInfo.ParseTZBuffer (name, buffer, buffer.Length);
460 internal static TimeZoneInfo GetTimeZone (string id)
463 if (id == "GMT" || id == "UTC")
464 return new TimeZoneInfo (id, TimeSpan.FromSeconds (0), id, id, id, null, true);
465 if (id.StartsWith ("GMT"))
466 return new TimeZoneInfo (id,
467 TimeSpan.FromSeconds (ParseNumericZone (id)),
468 id, id, id, null, true);
472 return _GetTimeZone (id);
473 } catch (Exception) {
478 static int ParseNumericZone (string name)
480 if (name == null || !name.StartsWith ("GMT") || name.Length <= 3)
486 else if (name [3] == '-')
494 for (where = 4; where < name.Length; where++) {
495 char c = name [where];
503 if (c >= '0' && c <= '9')
504 hour = hour * 10 + c - '0';
510 for (; where < name.Length; where++) {
511 char c = name [where];
513 if (c >= '0' && c <= '9')
514 min = min * 10 + c - '0';
520 return sign * (hour * 60 + min) * 60;
521 else if (hour >= 100)
522 return sign * ((hour / 100) * 60 + (hour % 100)) * 60;
524 return sign * (hour * 60) * 60;
527 static readonly object _lock = new object ();
529 static TimeZoneInfo defaultZone;
530 internal static TimeZoneInfo Default {
533 if (defaultZone != null)
535 return defaultZone = GetTimeZone (GetDefaultTimeZoneName ());
540 // <sys/system_properties.h>
541 [DllImport ("/system/lib/libc.so")]
542 static extern int __system_property_get (string name, StringBuilder value);
544 const int MaxPropertyNameLength = 32; // <sys/system_properties.h>
545 const int MaxPropertyValueLength = 92; // <sys/system_properties.h>
547 static string GetDefaultTimeZoneName ()
549 var buf = new StringBuilder (MaxPropertyValueLength + 1);
550 int n = __system_property_get ("persist.sys.timezone", buf);
552 return buf.ToString ();
559 * mcs /out:tzi.exe /unsafe "/d:INSIDE_CORLIB;MONODROID;NET_4_0;LIBC;SELF_TEST" System/TimeZone*.cs ../../build/common/Consts.cs ../Mono.Options/Mono.Options/Options.cs
561 * mkdir -p usr/share/zoneinfo
562 * android_root=`adb shell echo '$ANDROID_ROOT' | tr -d "\r"`
563 * adb pull $android_root/usr/share/zoneinfo usr/share/zoneinfo
565 * ANDROID_ROOT=`pwd` mono tzi.exe
567 static void Main (string[] args)
569 Func<IAndroidTimeZoneDB> c = () => GetDefaultTimeZoneDB ();
570 Mono.Options.OptionSet p = null;
571 p = new Mono.Options.OptionSet () {
572 { "T=", "Create AndroidTzData from {PATH}.", v => {
573 c = () => new AndroidTzData (v);
575 { "Z=", "Create ZoneInfoDB from {DIR}.", v => {
576 c = () => new ZoneInfoDB (v);
578 { "help", "Show this message and exit", v => {
579 p.WriteOptionDescriptions (Console.Out);
580 Environment.Exit (0);
584 AndroidTimeZones.db = c ();
585 Console.WriteLine ("DB type: {0}", AndroidTimeZones.db.GetType ().FullName);
586 foreach (var id in GetAvailableIds ()) {
587 Console.Write ("name={0,-40}", id);
589 TimeZoneInfo zone = _GetTimeZone (id);
591 Console.Write (" {0}", zone);
593 Console.Write (" ERROR:null");
595 } catch (Exception e) {
596 Console.WriteLine ();
597 Console.Write ("ERROR: {0}", e);
599 Console.WriteLine ();