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