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 android.system.ErrnoException; 20 import java.io.IOException; 21 import java.nio.charset.StandardCharsets; 22 import java.util.ArrayList; 23 import java.util.Arrays; 24 import java.util.List; 25 import libcore.io.BufferIterator; 26 import libcore.io.MemoryMappedFile; 27 28 /** 29 * A class used to initialize the time zone database. This implementation uses the 30 * Olson tzdata as the source of time zone information. However, to conserve 31 * disk space (inodes) and reduce I/O, all the data is concatenated into a single file, 32 * with an index to indicate the starting position of each time zone record. 33 * 34 * @hide - used to implement TimeZone 35 */ 36 public final class ZoneInfoDB { 37 private static final TzData DATA = 38 new TzData(System.getenv("ANDROID_DATA") + "/misc/zoneinfo/current/tzdata", 39 System.getenv("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata"); 40 41 public static class TzData { 42 /** 43 * Rather than open, read, and close the big data file each time we look up a time zone, 44 * we map the big data file during startup, and then just use the MemoryMappedFile. 45 * 46 * At the moment, this "big" data file is about 500 KiB. At some point, that will be small 47 * enough that we could just keep the byte[] in memory, but using mmap(2) like this has the 48 * nice property that even if someone replaces the file under us (because multiple gservices 49 * updates have gone out, say), we still get a consistent (if outdated) view of the world. 50 */ 51 private MemoryMappedFile mappedFile; 52 53 private String version; 54 private String zoneTab; 55 56 /** 57 * The 'ids' array contains time zone ids sorted alphabetically, for binary searching. 58 * The other two arrays are in the same order. 'byteOffsets' gives the byte offset 59 * of each time zone, and 'rawUtcOffsetsCache' gives the time zone's raw UTC offset. 60 */ 61 private String[] ids; 62 private int[] byteOffsets; 63 private int[] rawUtcOffsetsCache; // Access this via getRawUtcOffsets instead. 64 65 /** 66 * ZoneInfo objects are worth caching because they are expensive to create. 67 * See http://b/8270865 for context. 68 */ 69 private final static int CACHE_SIZE = 1; 70 private final BasicLruCache<String, ZoneInfo> cache = 71 new BasicLruCache<String, ZoneInfo>(CACHE_SIZE) { 72 @Override 73 protected ZoneInfo create(String id) { 74 BufferIterator it = getBufferIterator(id); 75 if (it == null) { 76 return null; 77 } 78 79 return ZoneInfo.makeTimeZone(id, it); 80 } 81 }; 82 83 public TzData(String... paths) { 84 for (String path : paths) { 85 if (loadData(path)) { 86 return; 87 } 88 } 89 90 // We didn't find any usable tzdata on disk, so let's just hard-code knowledge of "GMT". 91 // This is actually implemented in TimeZone itself, so if this is the only time zone 92 // we report, we won't be asked any more questions. 93 System.logE("Couldn't find any tzdata!"); 94 version = "missing"; 95 zoneTab = "# Emergency fallback data.\n"; 96 ids = new String[] { "GMT" }; 97 byteOffsets = rawUtcOffsetsCache = new int[1]; 98 } 99 100 /** 101 * Visible for testing. 102 */ 103 public BufferIterator getBufferIterator(String id) { 104 // Work out where in the big data file this time zone is. 105 int index = Arrays.binarySearch(ids, id); 106 if (index < 0) { 107 return null; 108 } 109 110 BufferIterator it = mappedFile.bigEndianIterator(); 111 it.skip(byteOffsets[index]); 112 return it; 113 } 114 115 private boolean loadData(String path) { 116 try { 117 mappedFile = MemoryMappedFile.mmapRO(path); 118 } catch (ErrnoException errnoException) { 119 return false; 120 } 121 try { 122 readHeader(); 123 return true; 124 } catch (Exception ex) { 125 // Something's wrong with the file. 126 // Log the problem and return false so we try the next choice. 127 System.logE("tzdata file \"" + path + "\" was present but invalid!", ex); 128 return false; 129 } 130 } 131 132 private void readHeader() { 133 // byte[12] tzdata_version -- "tzdata2012f\0" 134 // int index_offset 135 // int data_offset 136 // int zonetab_offset 137 BufferIterator it = mappedFile.bigEndianIterator(); 138 139 byte[] tzdata_version = new byte[12]; 140 it.readByteArray(tzdata_version, 0, tzdata_version.length); 141 String magic = new String(tzdata_version, 0, 6, StandardCharsets.US_ASCII); 142 if (!magic.equals("tzdata") || tzdata_version[11] != 0) { 143 throw new RuntimeException("bad tzdata magic: " + Arrays.toString(tzdata_version)); 144 } 145 version = new String(tzdata_version, 6, 5, StandardCharsets.US_ASCII); 146 147 int index_offset = it.readInt(); 148 int data_offset = it.readInt(); 149 int zonetab_offset = it.readInt(); 150 151 readIndex(it, index_offset, data_offset); 152 readZoneTab(it, zonetab_offset, (int) mappedFile.size() - zonetab_offset); 153 } 154 155 private void readZoneTab(BufferIterator it, int zoneTabOffset, int zoneTabSize) { 156 byte[] bytes = new byte[zoneTabSize]; 157 it.seek(zoneTabOffset); 158 it.readByteArray(bytes, 0, bytes.length); 159 zoneTab = new String(bytes, 0, bytes.length, StandardCharsets.US_ASCII); 160 } 161 162 private void readIndex(BufferIterator it, int indexOffset, int dataOffset) { 163 it.seek(indexOffset); 164 165 // The database reserves 40 bytes for each id. 166 final int SIZEOF_TZNAME = 40; 167 // The database uses 32-bit (4 byte) integers. 168 final int SIZEOF_TZINT = 4; 169 170 byte[] idBytes = new byte[SIZEOF_TZNAME]; 171 int indexSize = (dataOffset - indexOffset); 172 int entryCount = indexSize / (SIZEOF_TZNAME + 3*SIZEOF_TZINT); 173 174 char[] idChars = new char[entryCount * SIZEOF_TZNAME]; 175 int[] idEnd = new int[entryCount]; 176 int idOffset = 0; 177 178 byteOffsets = new int[entryCount]; 179 180 for (int i = 0; i < entryCount; i++) { 181 it.readByteArray(idBytes, 0, idBytes.length); 182 183 byteOffsets[i] = it.readInt(); 184 byteOffsets[i] += dataOffset; // TODO: change the file format so this is included. 185 186 int length = it.readInt(); 187 if (length < 44) { 188 throw new AssertionError("length in index file < sizeof(tzhead)"); 189 } 190 it.skip(4); // Skip the unused 4 bytes that used to be the raw offset. 191 192 // Don't include null chars in the String 193 int len = idBytes.length; 194 for (int j = 0; j < len; j++) { 195 if (idBytes[j] == 0) { 196 break; 197 } 198 idChars[idOffset++] = (char) (idBytes[j] & 0xFF); 199 } 200 201 idEnd[i] = idOffset; 202 } 203 204 // We create one string containing all the ids, and then break that into substrings. 205 // This way, all ids share a single char[] on the heap. 206 String allIds = new String(idChars, 0, idOffset); 207 ids = new String[entryCount]; 208 for (int i = 0; i < entryCount; i++) { 209 ids[i] = allIds.substring(i == 0 ? 0 : idEnd[i - 1], idEnd[i]); 210 } 211 } 212 213 public String[] getAvailableIDs() { 214 return ids.clone(); 215 } 216 217 public String[] getAvailableIDs(int rawUtcOffset) { 218 List<String> matches = new ArrayList<String>(); 219 int[] rawUtcOffsets = getRawUtcOffsets(); 220 for (int i = 0; i < rawUtcOffsets.length; ++i) { 221 if (rawUtcOffsets[i] == rawUtcOffset) { 222 matches.add(ids[i]); 223 } 224 } 225 return matches.toArray(new String[matches.size()]); 226 } 227 228 private synchronized int[] getRawUtcOffsets() { 229 if (rawUtcOffsetsCache != null) { 230 return rawUtcOffsetsCache; 231 } 232 rawUtcOffsetsCache = new int[ids.length]; 233 for (int i = 0; i < ids.length; ++i) { 234 // This creates a TimeZone, which is quite expensive. Hence the cache. 235 // Note that icu4c does the same (without the cache), so if you're 236 // switching this code over to icu4j you should check its performance. 237 // Telephony shouldn't care, but someone converting a bunch of calendar 238 // events might. 239 rawUtcOffsetsCache[i] = cache.get(ids[i]).getRawOffset(); 240 } 241 return rawUtcOffsetsCache; 242 } 243 244 public String getVersion() { 245 return version; 246 } 247 248 public String getZoneTab() { 249 return zoneTab; 250 } 251 252 public ZoneInfo makeTimeZone(String id) throws IOException { 253 ZoneInfo zoneInfo = cache.get(id); 254 // The object from the cache is cloned because TimeZone / ZoneInfo are mutable. 255 return zoneInfo == null ? null : (ZoneInfo) zoneInfo.clone(); 256 } 257 258 public boolean hasTimeZone(String id) throws IOException { 259 return cache.get(id) != null; 260 } 261 262 @Override protected void finalize() throws Throwable { 263 if (mappedFile != null) { 264 mappedFile.close(); 265 } 266 super.finalize(); 267 } 268 } 269 270 private ZoneInfoDB() { 271 } 272 273 public static TzData getInstance() { 274 return DATA; 275 } 276 } 277