ZoneInfoDB.java revision 6cbefca623f55004ba65f11577fc25f92f6297dc
1/*
2 * Copyright (C) 2007 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 libcore.util;
18
19import java.io.IOException;
20import java.io.RandomAccessFile;
21import java.nio.ByteBuffer;
22import java.nio.ByteOrder;
23import java.nio.channels.FileChannel.MapMode;
24import java.nio.charset.Charsets;
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.List;
28import java.util.TimeZone;
29import libcore.io.BufferIterator;
30import libcore.io.ErrnoException;
31import libcore.io.IoUtils;
32import libcore.io.MemoryMappedFile;
33
34// TODO: repackage this class, used by frameworks/base.
35import org.apache.harmony.luni.internal.util.TimezoneGetter;
36
37/**
38 * A class used to initialize the time zone database. This implementation uses the
39 * Olson tzdata as the source of time zone information. However, to conserve
40 * disk space (inodes) and reduce I/O, all the data is concatenated into a single file,
41 * with an index to indicate the starting position of each time zone record.
42 *
43 * @hide - used to implement TimeZone
44 */
45public final class ZoneInfoDB {
46    private static final Object LOCK = new Object();
47
48    /**
49     * Rather than open, read, and close the big data file each time we look up a time zone,
50     * we map the big data file during startup, and then just use the MemoryMappedFile.
51     *
52     * At the moment, this "big" data file is about 500 KiB. At some point, that will be small
53     * enough that we'll just keep the byte[] in memory.
54     */
55    private static final MemoryMappedFile TZDATA = mapData();
56
57    private static String version;
58
59    /**
60     * The 'ids' array contains time zone ids sorted alphabetically, for binary searching.
61     * The other two arrays are in the same order. 'byteOffsets' gives the byte offset
62     * of each time zone, and 'rawUtcOffsets' gives the time zone's raw UTC offset.
63     */
64    private static String[] ids;
65    private static int[] byteOffsets;
66    private static int[] rawUtcOffsets;
67
68    static {
69        readHeader();
70    }
71
72    private ZoneInfoDB() {
73    }
74
75    private static void readHeader() {
76        // byte[12] tzdata_version  -- "tzdata2012f\0"
77        // int file_format_version  -- 1
78        // int index_offset
79        // int data_offset
80        // int zonetab_offset
81        BufferIterator it = TZDATA.bigEndianIterator();
82
83        byte[] tzdata_version = new byte[12];
84        it.readByteArray(tzdata_version, 0, tzdata_version.length);
85        String magic = new String(tzdata_version, 0, 6, Charsets.US_ASCII);
86        if (!magic.equals("tzdata") || tzdata_version[11] != 0) {
87            throw new RuntimeException("bad tzdata magic: " + Arrays.toString(tzdata_version));
88        }
89        version = new String(tzdata_version, 6, 5, Charsets.US_ASCII);
90
91        int file_format_version = it.readInt();
92        if (file_format_version != 1) {
93            throw new RuntimeException("unknown tzdata file format version: " + file_format_version);
94        }
95
96        int index_offset = it.readInt();
97        int data_offset = it.readInt();
98        int zonetab_offset = it.readInt();
99        if (zonetab_offset != 0) {
100            throw new RuntimeException("non-zero zonetab offset: " + zonetab_offset);
101        }
102
103        readIndex(it, index_offset, data_offset);
104    }
105
106    private static MemoryMappedFile mapData() {
107        try {
108            String path = System.getenv("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata";
109            return MemoryMappedFile.mmapRO(path);
110        } catch (ErrnoException errnoException) {
111            throw new AssertionError(errnoException);
112        }
113    }
114
115    private static void readIndex(BufferIterator it, int indexOffset, int dataOffset) {
116        it.seek(indexOffset);
117
118        // The database reserves 40 bytes for each id.
119        final int SIZEOF_TZNAME = 40;
120        // The database uses 32-bit (4 byte) integers.
121        final int SIZEOF_TZINT = 4;
122
123        byte[] idBytes = new byte[SIZEOF_TZNAME];
124        int indexSize = (dataOffset - indexOffset);
125        int entryCount = indexSize / (SIZEOF_TZNAME + 3*SIZEOF_TZINT);
126
127        char[] idChars = new char[entryCount * SIZEOF_TZNAME];
128        int[] idEnd = new int[entryCount];
129        int idOffset = 0;
130
131        byteOffsets = new int[entryCount];
132        rawUtcOffsets = new int[entryCount];
133
134        for (int i = 0; i < entryCount; i++) {
135            it.readByteArray(idBytes, 0, idBytes.length);
136
137            byteOffsets[i] = it.readInt();
138            byteOffsets[i] += dataOffset; // TODO: change the file format so this is included.
139
140            int length = it.readInt();
141            if (length < 44) {
142                throw new AssertionError("length in index file < sizeof(tzhead)");
143            }
144            rawUtcOffsets[i] = it.readInt();
145
146            // Don't include null chars in the String
147            int len = idBytes.length;
148            for (int j = 0; j < len; j++) {
149                if (idBytes[j] == 0) {
150                    break;
151                }
152                idChars[idOffset++] = (char) (idBytes[j] & 0xFF);
153            }
154
155            idEnd[i] = idOffset;
156        }
157
158        // We create one string containing all the ids, and then break that into substrings.
159        // This way, all ids share a single char[] on the heap.
160        String allIds = new String(idChars, 0, idOffset);
161        ids = new String[entryCount];
162        for (int i = 0; i < entryCount; i++) {
163            ids[i] = allIds.substring(i == 0 ? 0 : idEnd[i - 1], idEnd[i]);
164        }
165    }
166
167    public static TimeZone makeTimeZone(String id) throws IOException {
168        // Work out where in the big data file this time zone is.
169        int index = Arrays.binarySearch(ids, id);
170        if (index < 0) {
171            return null;
172        }
173
174        BufferIterator data = TZDATA.bigEndianIterator();
175        data.skip(byteOffsets[index]);
176
177        // Variable names beginning tzh_ correspond to those in "tzfile.h".
178        // Check tzh_magic.
179        if (data.readInt() != 0x545a6966) { // "TZif"
180            return null;
181        }
182
183        // Skip the uninteresting part of the header.
184        data.skip(28);
185
186        // Read the sizes of the arrays we're about to read.
187        int tzh_timecnt = data.readInt();
188        int tzh_typecnt = data.readInt();
189
190        data.skip(4); // Skip tzh_charcnt.
191
192        int[] transitions = new int[tzh_timecnt];
193        data.readIntArray(transitions, 0, transitions.length);
194
195        byte[] type = new byte[tzh_timecnt];
196        data.readByteArray(type, 0, type.length);
197
198        int[] gmtOffsets = new int[tzh_typecnt];
199        byte[] isDsts = new byte[tzh_typecnt];
200        for (int i = 0; i < tzh_typecnt; ++i) {
201            gmtOffsets[i] = data.readInt();
202            isDsts[i] = data.readByte();
203            // We skip the abbreviation index. This would let us provide historically-accurate
204            // time zone abbreviations (such as "AHST", "YST", and "AKST" for standard time in
205            // America/Anchorage in 1982, 1983, and 1984 respectively). ICU only knows the current
206            // names, though, so even if we did use this data to provide the correct abbreviations
207            // for en_US, we wouldn't be able to provide correct abbreviations for other locales,
208            // nor would we be able to provide correct long forms (such as "Yukon Standard Time")
209            // for any locale. (The RI doesn't do any better than us here either.)
210            data.skip(1);
211        }
212
213        return new ZoneInfo(id, transitions, type, gmtOffsets, isDsts);
214    }
215
216    public static String[] getAvailableIDs() {
217        return ids.clone();
218    }
219
220    public static String[] getAvailableIDs(int rawOffset) {
221        List<String> matches = new ArrayList<String>();
222        for (int i = 0, end = rawUtcOffsets.length; i < end; ++i) {
223            if (rawUtcOffsets[i] == rawOffset) {
224                matches.add(ids[i]);
225            }
226        }
227        return matches.toArray(new String[matches.size()]);
228    }
229
230    public static TimeZone getSystemDefault() {
231        synchronized (LOCK) {
232            TimezoneGetter tzGetter = TimezoneGetter.getInstance();
233            String zoneName = tzGetter != null ? tzGetter.getId() : null;
234            if (zoneName != null) {
235                zoneName = zoneName.trim();
236            }
237            if (zoneName == null || zoneName.isEmpty()) {
238                // use localtime for the simulator
239                // TODO: what does that correspond to?
240                zoneName = "localtime";
241            }
242            return TimeZone.getTimeZone(zoneName);
243        }
244    }
245
246    public static String getVersion() {
247        return version;
248    }
249}
250