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