ZoneInfoDB.java revision 9b510df35b57946d843ffc34cf23fdcfc84c5220
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 * 'zoneinfo' database as the source of time zone information.  However, to conserve
40 * disk space the data for all time zones are concatenated into a single file, and a
41 * second file is used to indicate the starting position of each time zone record.  A
42 * third file indicates the version of the zoneinfo database used to generate the data.
43 *
44 * @hide - used to implement TimeZone
45 */
46public final class ZoneInfoDB {
47    /**
48     * The directory containing the time zone database files.
49     */
50    private static final String ZONE_DIRECTORY_NAME =
51            System.getenv("ANDROID_ROOT") + "/usr/share/zoneinfo/";
52
53    /**
54     * The name of the file containing the concatenated time zone records.
55     */
56    private static final String ZONE_FILE_NAME = ZONE_DIRECTORY_NAME + "zoneinfo.dat";
57
58    /**
59     * The name of the file containing the index to each time zone record within
60     * the zoneinfo.dat file.
61     */
62    private static final String INDEX_FILE_NAME = ZONE_DIRECTORY_NAME + "zoneinfo.idx";
63
64    private static final Object LOCK = new Object();
65
66    private static final String VERSION = readVersion();
67
68    /**
69     * Rather than open, read, and close the big data file each time we look up a time zone,
70     * we map the big data file during startup, and then just use the MemoryMappedFile.
71     *
72     * At the moment, this "big" data file is about 500 KiB. At some point, that will be small
73     * enough that we'll just keep the byte[] in memory.
74     */
75    private static final MemoryMappedFile ALL_ZONE_DATA = mapData();
76
77    /**
78     * The 'ids' array contains time zone ids sorted alphabetically, for binary searching.
79     * The other two arrays are in the same order. 'byteOffsets' gives the byte offset
80     * into "zoneinfo.dat" of each time zone, and 'rawUtcOffsets' gives the time zone's
81     * raw UTC offset.
82     */
83    private static String[] ids;
84    private static int[] byteOffsets;
85    private static int[] rawUtcOffsets;
86    static {
87        readIndex();
88    }
89
90    private ZoneInfoDB() {
91    }
92
93    /**
94     * Reads the file indicating the database version in use.
95     */
96    private static String readVersion() {
97        try {
98            byte[] bytes = IoUtils.readFileAsByteArray(ZONE_DIRECTORY_NAME + "zoneinfo.version");
99            return new String(bytes, 0, bytes.length, Charsets.ISO_8859_1).trim();
100        } catch (IOException ex) {
101            throw new RuntimeException(ex);
102        }
103    }
104
105    private static MemoryMappedFile mapData() {
106        try {
107            return MemoryMappedFile.mmapRO(ZONE_FILE_NAME);
108        } catch (ErrnoException errnoException) {
109            throw new AssertionError(errnoException);
110        }
111    }
112
113    /**
114     * Traditionally, Unix systems have one file per time zone. We have one big data file, which
115     * is just a concatenation of regular time zone files. To allow random access into this big
116     * data file, we also have an index. We read the index at startup, and keep it in memory so
117     * we can binary search by id when we need time zone data.
118     *
119     * The format of this file is, I believe, Android's own, and undocumented.
120     *
121     * All this code assumes strings are US-ASCII.
122     */
123    private static void readIndex() {
124        MemoryMappedFile mappedFile = null;
125        try {
126            mappedFile = MemoryMappedFile.mmapRO(INDEX_FILE_NAME);
127            readIndex(mappedFile);
128        } catch (Exception ex) {
129            throw new AssertionError(ex);
130        } finally {
131            IoUtils.closeQuietly(mappedFile);
132        }
133    }
134
135    private static void readIndex(MemoryMappedFile mappedFile) throws ErrnoException, IOException {
136        BufferIterator it = mappedFile.bigEndianIterator();
137
138        // The database reserves 40 bytes for each id.
139        final int SIZEOF_TZNAME = 40;
140        // The database uses 32-bit (4 byte) integers.
141        final int SIZEOF_TZINT = 4;
142
143        byte[] idBytes = new byte[SIZEOF_TZNAME];
144        int numEntries = (int) mappedFile.size() / (SIZEOF_TZNAME + 3*SIZEOF_TZINT);
145
146        char[] idChars = new char[numEntries * SIZEOF_TZNAME];
147        int[] idEnd = new int[numEntries];
148        int idOffset = 0;
149
150        byteOffsets = new int[numEntries];
151        rawUtcOffsets = new int[numEntries];
152
153        for (int i = 0; i < numEntries; i++) {
154            it.readByteArray(idBytes, 0, idBytes.length);
155            byteOffsets[i] = it.readInt();
156            int length = it.readInt();
157            if (length < 44) {
158                throw new AssertionError("length in index file < sizeof(tzhead)");
159            }
160            rawUtcOffsets[i] = it.readInt();
161
162            // Don't include null chars in the String
163            int len = idBytes.length;
164            for (int j = 0; j < len; j++) {
165                if (idBytes[j] == 0) {
166                    break;
167                }
168                idChars[idOffset++] = (char) (idBytes[j] & 0xFF);
169            }
170
171            idEnd[i] = idOffset;
172        }
173
174        // We create one string containing all the ids, and then break that into substrings.
175        // This way, all ids share a single char[] on the heap.
176        String allIds = new String(idChars, 0, idOffset);
177        ids = new String[numEntries];
178        for (int i = 0; i < numEntries; i++) {
179            ids[i] = allIds.substring(i == 0 ? 0 : idEnd[i - 1], idEnd[i]);
180        }
181    }
182
183    private static TimeZone makeTimeZone(String id) throws IOException {
184        // Work out where in the big data file this time zone is.
185        int index = Arrays.binarySearch(ids, id);
186        if (index < 0) {
187            return null;
188        }
189
190        BufferIterator data = ALL_ZONE_DATA.bigEndianIterator();
191        data.skip(byteOffsets[index]);
192
193        // Variable names beginning tzh_ correspond to those in "tzfile.h".
194        // Check tzh_magic.
195        if (data.readInt() != 0x545a6966) { // "TZif"
196            return null;
197        }
198
199        // Skip the uninteresting part of the header.
200        data.skip(28);
201
202        // Read the sizes of the arrays we're about to read.
203        int tzh_timecnt = data.readInt();
204        int tzh_typecnt = data.readInt();
205
206        data.skip(4); // Skip tzh_charcnt.
207
208        int[] transitions = new int[tzh_timecnt];
209        data.readIntArray(transitions, 0, transitions.length);
210
211        byte[] type = new byte[tzh_timecnt];
212        data.readByteArray(type, 0, type.length);
213
214        int[] gmtOffsets = new int[tzh_typecnt];
215        byte[] isDsts = new byte[tzh_typecnt];
216        for (int i = 0; i < tzh_typecnt; ++i) {
217            gmtOffsets[i] = data.readInt();
218            isDsts[i] = data.readByte();
219            // We skip the abbreviation index. This would let us provide historically-accurate
220            // time zone abbreviations (such as "AHST", "YST", and "AKST" for standard time in
221            // America/Anchorage in 1982, 1983, and 1984 respectively). ICU only knows the current
222            // names, though, so even if we did use this data to provide the correct abbreviations
223            // for en_US, we wouldn't be able to provide correct abbreviations for other locales,
224            // nor would we be able to provide correct long forms (such as "Yukon Standard Time")
225            // for any locale. (The RI doesn't do any better than us here either.)
226            data.skip(1);
227        }
228
229        return new ZoneInfo(id, transitions, type, gmtOffsets, isDsts);
230    }
231
232    public static String[] getAvailableIDs() {
233        return ids.clone();
234    }
235
236    public static String[] getAvailableIDs(int rawOffset) {
237        List<String> matches = new ArrayList<String>();
238        for (int i = 0, end = rawUtcOffsets.length; i < end; i++) {
239            if (rawUtcOffsets[i] == rawOffset) {
240                matches.add(ids[i]);
241            }
242        }
243        return matches.toArray(new String[matches.size()]);
244    }
245
246    public static TimeZone getSystemDefault() {
247        synchronized (LOCK) {
248            TimezoneGetter tzGetter = TimezoneGetter.getInstance();
249            String zoneName = tzGetter != null ? tzGetter.getId() : null;
250            if (zoneName != null) {
251                zoneName = zoneName.trim();
252            }
253            if (zoneName == null || zoneName.isEmpty()) {
254                // use localtime for the simulator
255                // TODO: what does that correspond to?
256                zoneName = "localtime";
257            }
258            return TimeZone.getTimeZone(zoneName);
259        }
260    }
261
262    public static TimeZone getTimeZone(String id) {
263        if (id == null) {
264            return null;
265        }
266        try {
267            return makeTimeZone(id);
268        } catch (IOException ignored) {
269            return null;
270        }
271    }
272
273    public static String getVersion() {
274        return VERSION;
275    }
276}
277