Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2019 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 | import com.google.common.base.Joiner; |
| 18 | |
| 19 | import java.io.File; |
| 20 | import java.io.FileOutputStream; |
| 21 | import java.io.IOException; |
| 22 | import java.io.OutputStreamWriter; |
| 23 | import java.io.Writer; |
| 24 | import java.nio.MappedByteBuffer; |
| 25 | import java.nio.charset.StandardCharsets; |
| 26 | import java.time.Duration; |
| 27 | import java.time.Instant; |
| 28 | import java.util.ArrayList; |
| 29 | import java.util.List; |
| 30 | |
| 31 | /** |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 32 | * Dumps out the contents of a tzfile in a CSV form. |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 33 | * |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 34 | * <p>This class contains a near copy of logic found in Android's ZoneInfo class. |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 35 | */ |
| 36 | public class TzFileDumper { |
| 37 | |
| 38 | public static void main(String[] args) throws Exception { |
| 39 | if (args.length != 2) { |
| 40 | System.err.println("usage: java TzFileDumper <tzfile|dir> <output file|output dir>"); |
| 41 | System.exit(0); |
| 42 | } |
| 43 | |
| 44 | File input = new File(args[0]); |
| 45 | File output = new File(args[1]); |
| 46 | if (input.isDirectory()) { |
| 47 | if (!output.isDirectory()) { |
| 48 | System.err.println("If first args is a directory, second arg must be a directory"); |
| 49 | System.exit(1); |
| 50 | } |
| 51 | |
| 52 | for (File inputFile : input.listFiles()) { |
| 53 | if (inputFile.isFile()) { |
| 54 | File outputFile = new File(output, inputFile.getName() + ".csv"); |
| 55 | try { |
| 56 | new TzFileDumper(inputFile, outputFile).execute(); |
| 57 | } catch (IOException e) { |
| 58 | System.err.println("Error processing:" + inputFile); |
| 59 | } |
| 60 | } |
| 61 | } |
| 62 | } else { |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 63 | new TzFileDumper(input, output).execute(); |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | private final File inputFile; |
| 68 | private final File outputFile; |
| 69 | |
| 70 | private TzFileDumper(File inputFile, File outputFile) { |
| 71 | this.inputFile = inputFile; |
| 72 | this.outputFile = outputFile; |
| 73 | } |
| 74 | |
| 75 | private void execute() throws IOException { |
| 76 | System.out.println("Dumping " + inputFile + " to " + outputFile); |
| 77 | MappedByteBuffer mappedTzFile = ZoneSplitter.createMappedByteBuffer(inputFile); |
| 78 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 79 | try (Writer fileWriter = new OutputStreamWriter( |
| 80 | new FileOutputStream(outputFile), StandardCharsets.UTF_8)) { |
| 81 | Header header32Bit = readHeader(mappedTzFile); |
| 82 | List<Transition> transitions32Bit = read32BitTransitions(mappedTzFile, header32Bit); |
| 83 | List<Type> types32Bit = readTypes(mappedTzFile, header32Bit); |
| 84 | skipUninteresting32BitData(mappedTzFile, header32Bit); |
| 85 | types32Bit = mergeTodInfo(mappedTzFile, header32Bit, types32Bit); |
| 86 | |
| 87 | writeCsvRow(fileWriter, "File format version: " + (char) header32Bit.tzh_version); |
| 88 | writeCsvRow(fileWriter); |
| 89 | writeCsvRow(fileWriter, "32-bit data"); |
| 90 | writeCsvRow(fileWriter); |
| 91 | writeTypes(types32Bit, fileWriter); |
| 92 | writeCsvRow(fileWriter); |
| 93 | writeTransitions(transitions32Bit, types32Bit, fileWriter); |
| 94 | writeCsvRow(fileWriter); |
| 95 | |
| 96 | if (header32Bit.tzh_version >= '2') { |
| 97 | Header header64Bit = readHeader(mappedTzFile); |
| 98 | List<Transition> transitions64Bit = read64BitTransitions(mappedTzFile, header64Bit); |
| 99 | List<Type> types64Bit = readTypes(mappedTzFile, header64Bit); |
| 100 | skipUninteresting64BitData(mappedTzFile, header64Bit); |
| 101 | types64Bit = mergeTodInfo(mappedTzFile, header64Bit, types64Bit); |
| 102 | |
| 103 | writeCsvRow(fileWriter, "64-bit data"); |
| 104 | writeCsvRow(fileWriter); |
| 105 | writeTypes(types64Bit, fileWriter); |
| 106 | writeCsvRow(fileWriter); |
| 107 | writeTransitions(transitions64Bit, types64Bit, fileWriter); |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | private Header readHeader(MappedByteBuffer mappedTzFile) throws IOException { |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 113 | // Variable names beginning tzh_ correspond to those in "tzfile.h". |
| 114 | // Check tzh_magic. |
| 115 | int tzh_magic = mappedTzFile.getInt(); |
| 116 | if (tzh_magic != 0x545a6966) { // "TZif" |
| 117 | throw new IOException("File=" + inputFile + " has an invalid header=" + tzh_magic); |
| 118 | } |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 119 | |
| 120 | byte tzh_version = mappedTzFile.get(); |
| 121 | |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 122 | // Skip the uninteresting part of the header. |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 123 | mappedTzFile.position(mappedTzFile.position() + 15); |
| 124 | int tzh_ttisgmtcnt = mappedTzFile.getInt(); |
| 125 | int tzh_ttisstdcnt = mappedTzFile.getInt(); |
| 126 | int tzh_leapcnt = mappedTzFile.getInt(); |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 127 | |
| 128 | // Read the sizes of the arrays we're about to read. |
| 129 | int tzh_timecnt = mappedTzFile.getInt(); |
| 130 | // Arbitrary ceiling to prevent allocating memory for corrupt data. |
| 131 | // 2 per year with 2^32 seconds would give ~272 transitions. |
| 132 | final int MAX_TRANSITIONS = 2000; |
| 133 | if (tzh_timecnt < 0 || tzh_timecnt > MAX_TRANSITIONS) { |
| 134 | throw new IOException( |
| 135 | "File=" + inputFile + " has an invalid number of transitions=" + tzh_timecnt); |
| 136 | } |
| 137 | |
| 138 | int tzh_typecnt = mappedTzFile.getInt(); |
| 139 | final int MAX_TYPES = 256; |
| 140 | if (tzh_typecnt < 1) { |
| 141 | throw new IOException("ZoneInfo requires at least one type to be provided for each" |
| 142 | + " timezone but could not find one for '" + inputFile + "'"); |
| 143 | } else if (tzh_typecnt > MAX_TYPES) { |
| 144 | throw new IOException( |
| 145 | "File=" + inputFile + " has too many types=" + tzh_typecnt); |
| 146 | } |
| 147 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 148 | int tzh_charcnt = mappedTzFile.getInt(); |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 149 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 150 | return new Header( |
| 151 | tzh_version, tzh_ttisgmtcnt, tzh_ttisstdcnt, tzh_leapcnt, tzh_timecnt, tzh_typecnt, |
| 152 | tzh_charcnt); |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 153 | } |
| 154 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 155 | private List<Transition> read32BitTransitions(MappedByteBuffer mappedTzFile, Header header) |
| 156 | throws IOException { |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 157 | |
| 158 | // Read the data. |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 159 | int[] transitionTimes = new int[header.tzh_timecnt]; |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 160 | fillIntArray(mappedTzFile, transitionTimes); |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 161 | |
| 162 | byte[] typeIndexes = new byte[header.tzh_timecnt]; |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 163 | mappedTzFile.get(typeIndexes); |
| 164 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 165 | // Convert int times to longs |
| 166 | long[] transitionTimesLong = new long[header.tzh_timecnt]; |
| 167 | for (int i = 0; i < header.tzh_timecnt; ++i) { |
| 168 | transitionTimesLong[i] = transitionTimes[i]; |
| 169 | } |
| 170 | |
| 171 | return createTransitions(header, transitionTimesLong, typeIndexes); |
| 172 | } |
| 173 | |
| 174 | private List<Transition> createTransitions(Header header, |
| 175 | long[] transitionTimes, byte[] typeIndexes) throws IOException { |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 176 | List<Transition> transitions = new ArrayList<>(); |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 177 | for (int i = 0; i < header.tzh_timecnt; ++i) { |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 178 | if (i > 0 && transitionTimes[i] <= transitionTimes[i - 1]) { |
| 179 | throw new IOException( |
| 180 | inputFile + " transition at " + i + " is not sorted correctly, is " |
| 181 | + transitionTimes[i] + ", previous is " + transitionTimes[i - 1]); |
| 182 | } |
| 183 | |
| 184 | int typeIndex = typeIndexes[i] & 0xff; |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 185 | if (typeIndex >= header.tzh_typecnt) { |
| 186 | throw new IOException(inputFile + " type at " + i + " is not < " |
| 187 | + header.tzh_typecnt + ", is " + typeIndex); |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 188 | } |
| 189 | |
| 190 | Transition transition = new Transition(transitionTimes[i], typeIndex); |
| 191 | transitions.add(transition); |
| 192 | } |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 193 | return transitions; |
| 194 | } |
| 195 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 196 | private List<Transition> read64BitTransitions(MappedByteBuffer mappedTzFile, Header header) |
| 197 | throws IOException { |
| 198 | long[] transitionTimes = new long[header.tzh_timecnt]; |
| 199 | fillLongArray(mappedTzFile, transitionTimes); |
| 200 | |
| 201 | byte[] typeIndexes = new byte[header.tzh_timecnt]; |
| 202 | mappedTzFile.get(typeIndexes); |
| 203 | |
| 204 | return createTransitions(header, transitionTimes, typeIndexes); |
| 205 | } |
| 206 | |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 207 | private void writeTransitions(List<Transition> transitions, List<Type> types, Writer fileWriter) |
| 208 | throws IOException { |
| 209 | |
| 210 | List<Object[]> rows = new ArrayList<>(); |
| 211 | for (Transition transition : transitions) { |
| 212 | Type type = types.get(transition.typeIndex); |
| 213 | Object[] row = new Object[] { |
| 214 | transition.transitionTimeSeconds, |
| 215 | transition.typeIndex, |
| 216 | formatTimeSeconds(transition.transitionTimeSeconds), |
| 217 | formatDurationSeconds(type.gmtOffsetSeconds), |
| 218 | formatIsDst(type.isDst), |
| 219 | }; |
| 220 | rows.add(row); |
| 221 | } |
| 222 | |
| 223 | writeCsvRow(fileWriter, "Transitions"); |
| 224 | writeTuplesCsv(fileWriter, rows, "transition", "type", "[UTC time]", "[Type offset]", |
| 225 | "[Type isDST]"); |
| 226 | } |
| 227 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 228 | private List<Type> readTypes(MappedByteBuffer mappedTzFile, Header header) throws IOException { |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 229 | List<Type> types = new ArrayList<>(); |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 230 | for (int i = 0; i < header.tzh_typecnt; ++i) { |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 231 | int gmtOffsetSeconds = mappedTzFile.getInt(); |
| 232 | byte isDst = mappedTzFile.get(); |
| 233 | if (isDst != 0 && isDst != 1) { |
| 234 | throw new IOException(inputFile + " dst at " + i + " is not 0 or 1, is " + isDst); |
| 235 | } |
| 236 | |
| 237 | // We skip the abbreviation index. |
| 238 | mappedTzFile.get(); |
| 239 | |
| 240 | types.add(new Type(gmtOffsetSeconds, isDst)); |
| 241 | } |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 242 | return types; |
| 243 | } |
| 244 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 245 | private static void skipUninteresting32BitData(MappedByteBuffer mappedTzFile, Header header) { |
| 246 | mappedTzFile.get(new byte[header.tzh_charcnt]); |
| 247 | int leapInfoSize = 4 + 4; |
| 248 | mappedTzFile.get(new byte[header.tzh_leapcnt * leapInfoSize]); |
| 249 | } |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 250 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 251 | |
| 252 | private void skipUninteresting64BitData(MappedByteBuffer mappedTzFile, Header header) { |
| 253 | mappedTzFile.get(new byte[header.tzh_charcnt]); |
| 254 | int leapInfoSize = 8 + 4; |
| 255 | mappedTzFile.get(new byte[header.tzh_leapcnt * leapInfoSize]); |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * Populate ttisstd and ttisgmt information by copying {@code types} and populating those fields |
| 260 | * in the copies. |
| 261 | */ |
| 262 | private static List<Type> mergeTodInfo( |
| 263 | MappedByteBuffer mappedTzFile, Header header, List<Type> types) { |
| 264 | |
| 265 | byte[] ttisstds = new byte[header.tzh_ttisstdcnt]; |
| 266 | mappedTzFile.get(ttisstds); |
| 267 | byte[] ttisgmts = new byte[header.tzh_ttisgmtcnt]; |
| 268 | mappedTzFile.get(ttisgmts); |
| 269 | |
| 270 | List<Type> outputTypes = new ArrayList<>(); |
| 271 | for (int i = 0; i < types.size(); i++) { |
| 272 | Type inputType = types.get(i); |
| 273 | Byte ttisstd = ttisstds.length == 0 ? null : ttisstds[i]; |
| 274 | Byte ttisgmt = ttisgmts.length == 0 ? null : ttisgmts[i]; |
| 275 | Type outputType = |
| 276 | new Type(inputType.gmtOffsetSeconds, inputType.isDst, ttisstd, ttisgmt); |
| 277 | outputTypes.add(outputType); |
| 278 | } |
| 279 | return outputTypes; |
| 280 | } |
| 281 | |
| 282 | private void writeTypes(List<Type> types, Writer fileWriter) throws IOException { |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 283 | List<Object[]> rows = new ArrayList<>(); |
| 284 | for (Type type : types) { |
| 285 | Object[] row = new Object[] { |
| 286 | type.gmtOffsetSeconds, |
| 287 | type.isDst, |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 288 | type.ttisgmt, |
| 289 | type.ttisstd, |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 290 | formatDurationSeconds(type.gmtOffsetSeconds), |
| 291 | formatIsDst(type.isDst), |
| 292 | }; |
| 293 | rows.add(row); |
| 294 | } |
| 295 | |
| 296 | writeCsvRow(fileWriter, "Types"); |
| 297 | writeTuplesCsv( |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 298 | fileWriter, rows, "gmtOffset (seconds)", "isDst", "ttisgmt", "ttisstd", |
| 299 | "[gmtOffset ISO]", "[DST?]"); |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 300 | } |
| 301 | |
| 302 | private static void fillIntArray(MappedByteBuffer mappedByteBuffer, int[] toFill) { |
| 303 | for (int i = 0; i < toFill.length; i++) { |
| 304 | toFill[i] = mappedByteBuffer.getInt(); |
| 305 | } |
| 306 | } |
| 307 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 308 | private static void fillLongArray(MappedByteBuffer mappedByteBuffer, long[] toFill) { |
| 309 | for (int i = 0; i < toFill.length; i++) { |
| 310 | toFill[i] = mappedByteBuffer.getLong(); |
| 311 | } |
| 312 | } |
| 313 | |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 314 | private static String formatTimeSeconds(long timeInSeconds) { |
| 315 | long timeInMillis = timeInSeconds * 1000L; |
| 316 | return Instant.ofEpochMilli(timeInMillis).toString(); |
| 317 | } |
| 318 | |
| 319 | private static String formatDurationSeconds(int duration) { |
| 320 | return Duration.ofSeconds(duration).toString(); |
| 321 | } |
| 322 | |
| 323 | private String formatIsDst(byte isDst) { |
| 324 | return isDst == 0 ? "STD" : "DST"; |
| 325 | } |
| 326 | |
| 327 | private static void writeCsvRow(Writer writer, Object... values) throws IOException { |
| 328 | writer.append(Joiner.on(',').join(values)); |
| 329 | writer.append('\n'); |
| 330 | } |
| 331 | |
| 332 | private static void writeTuplesCsv(Writer writer, List<Object[]> lines, String... headings) |
| 333 | throws IOException { |
| 334 | |
| 335 | writeCsvRow(writer, (Object[]) headings); |
| 336 | for (Object[] line : lines) { |
| 337 | writeCsvRow(writer, line); |
| 338 | } |
| 339 | } |
| 340 | |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 341 | private static class Header { |
| 342 | |
| 343 | /** The version. Known values are 0 (ASCII NUL), 50 (ASCII '2'), 51 (ASCII '3'). */ |
| 344 | final byte tzh_version; |
| 345 | final int tzh_timecnt; |
| 346 | final int tzh_typecnt; |
| 347 | final int tzh_charcnt; |
| 348 | final int tzh_leapcnt; |
| 349 | final int tzh_ttisstdcnt; |
| 350 | final int tzh_ttisgmtcnt; |
| 351 | |
| 352 | Header(byte tzh_version, int tzh_ttisgmtcnt, int tzh_ttisstdcnt, int tzh_leapcnt, |
| 353 | int tzh_timecnt, int tzh_typecnt, int tzh_charcnt) { |
| 354 | this.tzh_version = tzh_version; |
| 355 | this.tzh_timecnt = tzh_timecnt; |
| 356 | this.tzh_typecnt = tzh_typecnt; |
| 357 | this.tzh_charcnt = tzh_charcnt; |
| 358 | this.tzh_leapcnt = tzh_leapcnt; |
| 359 | this.tzh_ttisstdcnt = tzh_ttisstdcnt; |
| 360 | this.tzh_ttisgmtcnt = tzh_ttisgmtcnt; |
| 361 | } |
| 362 | } |
| 363 | |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 364 | private static class Type { |
| 365 | |
| 366 | final int gmtOffsetSeconds; |
| 367 | final byte isDst; |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 368 | final Byte ttisstd; |
| 369 | final Byte ttisgmt; |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 370 | |
| 371 | Type(int gmtOffsetSeconds, byte isDst) { |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 372 | this(gmtOffsetSeconds, isDst, null, null); |
| 373 | } |
| 374 | |
| 375 | Type(int gmtOffsetSeconds, byte isDst, Byte ttisstd, Byte ttisgmt) { |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 376 | this.gmtOffsetSeconds = gmtOffsetSeconds; |
| 377 | this.isDst = isDst; |
Neil Fuller | bfcc985 | 2019-04-18 16:35:47 +0100 | [diff] [blame] | 378 | this.ttisstd = ttisstd; |
| 379 | this.ttisgmt = ttisgmt; |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 380 | } |
| 381 | } |
| 382 | |
| 383 | private static class Transition { |
| 384 | |
| 385 | final long transitionTimeSeconds; |
| 386 | final int typeIndex; |
| 387 | |
| 388 | Transition(long transitionTimeSeconds, int typeIndex) { |
| 389 | this.transitionTimeSeconds = transitionTimeSeconds; |
| 390 | this.typeIndex = typeIndex; |
| 391 | } |
| 392 | } |
| 393 | } |