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 17 package libcore.util; 18 19 import java.io.IOException; 20 import java.io.RandomAccessFile; 21 import java.nio.ByteBuffer; 22 import java.nio.ByteOrder; 23 import java.nio.channels.FileChannel.MapMode; 24 import java.nio.charset.Charsets; 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.List; 28 import java.util.TimeZone; 29 import libcore.io.BufferIterator; 30 import libcore.io.ErrnoException; 31 import libcore.io.IoUtils; 32 import libcore.io.MemoryMappedFile; 33 34 // TODO: repackage this class, used by frameworks/base. 35 import 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 */ 46 public 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