content: /*
   * Copyright (C) 2018 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.powermodel;
  
  import java.io.InputStream;
  import java.io.IOException;
  import java.lang.annotation.ElementType;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.lang.annotation.Target;
  import java.lang.reflect.Array;
  import java.lang.reflect.Field;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.ImmutableMap;
  import com.google.common.collect.ImmutableSet;
  
  public class RawBatteryStats {
      /**
       * The factory objects for the records. Initialized in the static block.
       */
      private static HashMap<String,RecordFactory> sFactories
              = new HashMap<String,RecordFactory>();
  
      /**
       * The Record objects that have been parsed.
       */
      private ArrayList<Record> mRecords = new ArrayList<Record>();
  
      /**
       * The Record objects that have been parsed, indexed by type.
       *
       * Don't use this before {@link #indexRecords()} has been called.
       */
      private ImmutableMap<String,ImmutableList<Record>> mRecordsByType;
  
      /**
       * The attribution keys for which we have data (corresponding to UIDs we've seen).
       * <p>
       * Does not include the synthetic apps.
       * <p>
       * Don't use this before {@link #indexRecords()} has been called.
       */
      private ImmutableSet<AttributionKey> mApps;
  
      /**
       * The warnings that have been issued during parsing.
       */
      private ArrayList<Warning> mWarnings = new ArrayList<Warning>();
  
      /**
       * The version of the BatteryStats dumpsys that we are using.  This value
       * is set to -1 initially, and then when parsing the (hopefully) first
       * line, 'vers', it is set to the correct version.
       */
      private int mDumpsysVersion = -1;
  
      /**
       * Enum used in the Line annotation to mark whether a field is expected to be
       * system-wide or scoped to an app.
       */
      public enum Scope {
          SYSTEM,
          UID
      }
  
      /**
       * Enum used to indicated the expected number of results.
       */
      public enum Count {
          SINGLE,
          MULTIPLE
      }
  
      /**
       * Annotates classes that represent a line of CSV in the batterystats CSV
       */
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      @interface Line {
          String tag();
          Scope scope();
          Count count();
      }
  
      /**
       * Annotates fields that should be parsed automatically from CSV
       */
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.FIELD)
      @interface Field {
          /**
           * The "column" of this field in the most recent version of the CSV.
           * When parsing old versions, fields that were added will be automatically
           * removed and the indices will be fixed up.
           *
           * The header fields (version, uid, category, type) will be automatically
           * handled for the base Line type.  The index 0 should start after those.
           */
          int index();
  
          /**
           * First version that this field appears in.
           */
          int added() default 0;
      }
  
      /**
       * Each line in the BatteryStats CSV is tagged with a category, that says
       * which of the time collection modes was used for the data.
       */
      public enum Category {
          INFO("i"),
          LAST("l"),
          UNPLUGGED("u"),
          CURRENT("c");
  
          public final String tag;
  
          Category(String tag) {
              this.tag = tag;
          }
      }
  
      /**
       * Base class for all lines in a batterystats CSV file.
       */
      public static class Record {
          /**
           * Whether all of the fields for the indicated version of this record
           * have been filled in.
           */
          public boolean complete;
  
  
          @Field(index=-4)
          public int lineVersion;
  
          @Field(index=-3)
          public int uid;
  
          @Field(index=-2)
          public Category category;
  
          @Field(index=-1)
          public String lineType;
      }
  
      @Line(tag="bt", scope=Scope.SYSTEM, count=Count.SINGLE)
      public static class Battery extends Record {
          // If which != STATS_SINCE_CHARGED, the csv will be "N/A" and we will get
          // a parsing warning.  Nobody uses anything other than STATS_SINCE_CHARGED.
          @Field(index=0)
          public int startCount;
  
          @Field(index=1)
          public long whichBatteryRealtimeMs;
  
          @Field(index=2)
          public long whichBatteryUptimeMs;
  
          @Field(index=3)
          public long totalRealtimeMs;
  
          @Field(index=4)
          public long totalUptimeMs;
  
          @Field(index=5)
          public long getStartClockTimeMs;
  
          @Field(index=6)
          public long whichBatteryScreenOffRealtimeMs;
  
          @Field(index=7)
          public long whichBatteryScreenOffUptimeMs;
  
          @Field(index=8)
          public long estimatedBatteryCapacityMah;
  
          @Field(index=9)
          public long minLearnedBatteryCapacityMah;
  
          @Field(index=10)
          public long maxLearnedBatteryCapacityMah;
  
          @Field(index=11)
          public long screenDozeTimeMs;
      }
  
      @Line(tag="gn", scope=Scope.SYSTEM, count=Count.SINGLE)
      public static class GlobalNetwork extends Record {
          @Field(index=0)
          public long mobileRxTotalBytes;
  
          @Field(index=1)
          public long mobileTxTotalBytes;
  
          @Field(index=2)
          public long wifiRxTotalBytes;
  
          @Field(index=3)
          public long wifiTxTotalBytes;
  
          @Field(index=4)
          public long mobileRxTotalPackets;
  
          @Field(index=5)
          public long mobileTxTotalPackets;
  
          @Field(index=6)
          public long wifiRxTotalPackets;
  
          @Field(index=7)
          public long wifiTxTotalPackets;
  
          @Field(index=8)
          public long btRxTotalBytes;
  
          @Field(index=9)
          public long btTxTotalBytes;
      }
  
      @Line(tag="gmcd", scope=Scope.SYSTEM, count=Count.SINGLE)
      public static class GlobalModemController extends Record {
          @Field(index=0)
          public long idleMs;
  
          @Field(index=1)
          public long rxTimeMs;
  
          @Field(index=2)
          public long powerMaMs;
  
          @Field(index=3)
          public long[] txTimeMs;
      }
  
      @Line(tag="m", scope=Scope.SYSTEM, count=Count.SINGLE)
      public static class Misc extends Record {
          @Field(index=0)
          public long screenOnTimeMs;
  
          @Field(index=1)
          public long phoneOnTimeMs;
  
          @Field(index=2)
          public long fullWakeLockTimeTotalMs;
  
          @Field(index=3)
          public long partialWakeLockTimeTotalMs;
  
          @Field(index=4)
          public long mobileRadioActiveTimeMs;
  
          @Field(index=5)
          public long mobileRadioActiveAdjustedTimeMs;
  
          @Field(index=6)
          public long interactiveTimeMs;
  
          @Field(index=7)
          public long powerSaveModeEnabledTimeMs;
  
          @Field(index=8)
          public int connectivityChangeCount;
  
          @Field(index=9)
          public long deepDeviceIdleModeTimeMs;
  
          @Field(index=10)
          public long deepDeviceIdleModeCount;
  
          @Field(index=11)
          public long deepDeviceIdlingTimeMs;
  
          @Field(index=12)
          public long deepDeviceIdlingCount;
  
          @Field(index=13)
          public long mobileRadioActiveCount;
  
          @Field(index=14)
          public long mobileRadioActiveUnknownTimeMs;
  
          @Field(index=15)
          public long lightDeviceIdleModeTimeMs;
  
          @Field(index=16)
          public long lightDeviceIdleModeCount;
  
          @Field(index=17)
          public long lightDeviceIdlingTimeMs;
  
          @Field(index=18)
          public long lightDeviceIdlingCount;
  
          @Field(index=19)
          public long lightLongestDeviceIdleModeTimeMs;
  
          @Field(index=20)
          public long deepLongestDeviceIdleModeTimeMs;
      }
  
      @Line(tag="nt", scope=Scope.UID, count=Count.SINGLE)
      public static class Network extends Record {
          @Field(index=0)
          public long mobileRxBytes;
  
          @Field(index=1)
          public long mobileTxBytes;
  
          @Field(index=2)
          public long wifiRxBytes;
  
          @Field(index=3)
          public long wifiTxBytes;
  
          @Field(index=4)
          public long mobileRxPackets;
  
          @Field(index=5)
          public long mobileTxPackets;
  
          @Field(index=6)
          public long wifiRxPackets;
  
          @Field(index=7)
          public long wifiTxPackets;
  
          // This is microseconds, because... batterystats.
          @Field(index=8)
          public long mobileRadioActiveTimeUs;
  
          @Field(index=9)
          public long mobileRadioActiveCount;
  
          @Field(index=10)
          public long btRxBytes;
  
          @Field(index=11)
          public long btTxBytes;
  
          @Field(index=12)
          public long mobileWakeupCount;
  
          @Field(index=13)
          public long wifiWakeupCount;
  
          @Field(index=14)
          public long mobileBgRxBytes;
  
          @Field(index=15)
          public long mobileBgTxBytes;
  
          @Field(index=16)
          public long wifiBgRxBytes;
  
          @Field(index=17)
          public long wifiBgTxBytes;
  
          @Field(index=18)
          public long mobileBgRxPackets;
  
          @Field(index=19)
          public long mobileBgTxPackets;
  
          @Field(index=20)
          public long wifiBgRxPackets;
  
          @Field(index=21)
          public long wifiBgTxPackets;
      }
  
      @Line(tag="sgt", scope=Scope.SYSTEM, count=Count.SINGLE)
      public static class SignalStrengthTime extends Record {
          @Field(index=0)
          public long[] phoneSignalStrengthTimeMs;
      }
  
      @Line(tag="sst", scope=Scope.SYSTEM, count=Count.SINGLE)
      public static class SignalScanningTime extends Record {
          @Field(index=0)
          public long phoneSignalScanningTimeMs;
      }
  
      @Line(tag="uid", scope=Scope.UID, count=Count.MULTIPLE)
      public static class Uid extends Record {
          @Field(index=0)
          public int uidKey;
  
          @Field(index=1)
          public String pkg;
      }
  
      @Line(tag="vers", scope=Scope.SYSTEM, count=Count.SINGLE)
      public static class Version extends Record {
          @Field(index=0)
          public int dumpsysVersion;
  
          @Field(index=1)
          public int parcelVersion;
  
          @Field(index=2)
          public String startPlatformVersion;
  
          @Field(index=3)
          public String endPlatformVersion;
      }
  
      /**
       * Codes for the warnings to classify warnings without parsing them.
       */
      public enum WarningId {
          /**
           * A row didn't have enough fields to match any records and let us extract
           * a line type.
           */
          TOO_FEW_FIELDS_FOR_LINE_TYPE,
  
          /**
           * We couldn't find a Record for the given line type.
           */
          NO_MATCHING_LINE_TYPE,
  
          /**
           * Couldn't set the value of a field. Usually this is because the
           * contents of a numeric type couldn't be parsed.
           */
          BAD_FIELD_TYPE,
  
          /**
           * There were extra field values in the input text.
           */
          TOO_MANY_FIELDS,
  
          /**
           * There were fields that we were expecting (for this version
           * of the dumpsys) that weren't provided in the CSV data.
           */
          NOT_ENOUGH_FIELDS,
  
          /**
           * The dumpsys version in the 'vers' CSV line couldn't be parsed.
           */
          BAD_DUMPSYS_VERSION
      }
  
      /**
       * A non-fatal problem detected during parsing.
       */
      public static class Warning {
          private int mLineNumber;
          private WarningId mId;
          private ArrayList<String> mFields;
          private String mMessage;
          private String[] mExtras;
  
          public Warning(int lineNumber, WarningId id, ArrayList<String> fields, String message,
                  String[] extras) {
              mLineNumber = lineNumber;
              mId = id;
              mFields = fields;
              mMessage = message;
              mExtras = extras;
          }
  
          public int getLineNumber() {
              return mLineNumber;
          }
  
          public ArrayList<String> getFields() {
              return mFields;
          }
  
          public String getMessage() {
              return mMessage;
          }
  
          public String[] getExtras() {
              return mExtras;
          }
      }
  
      /**
       * Base class for classes to set fields on Record objects via reflection.
       */
      private abstract static class FieldSetter {
          private int mIndex;
          private int mAdded;
          protected java.lang.reflect.Field mField;
  
          FieldSetter(int index, int added, java.lang.reflect.Field field) {
              mIndex = index;
              mAdded = added;
              mField = field;
          }
  
          String getName() {
              return mField.getName();
          }
  
          int getIndex() {
              return mIndex;
          }
  
          int getAdded() {
              return mAdded;
          }
  
          boolean isArray() {
              return mField.getType().isArray();
          }
  
          abstract void setField(int lineNumber, Record object, String value) throws ParseException;
          abstract void setArray(int lineNumber, Record object, ArrayList<String> values,
                  int startIndex, int endIndex) throws ParseException;
  
          @Override
          public String toString() {
              final String className = getClass().getName();
              int startIndex = Math.max(className.lastIndexOf('.'), className.lastIndexOf('$'));
              if (startIndex < 0) {
                  startIndex = 0;
              } else {
                  startIndex++;
              }
              return className.substring(startIndex) + "(index=" + mIndex + " added=" + mAdded
                      + " field=" + mField.getName()
                      + " type=" + mField.getType().getSimpleName()
                      + ")";
          }
      }
  
      /**
       * Sets int fields on Record objects using reflection.
       */
      private static class IntFieldSetter extends FieldSetter {
          IntFieldSetter(int index, int added, java.lang.reflect.Field field) {
              super(index, added, field);
          }
  
          void setField(int lineNumber, Record object, String value) throws ParseException {
              try {
                  mField.setInt(object, Integer.parseInt(value.trim()));
              } catch (NumberFormatException ex) {
                  throw new ParseException(lineNumber, "can't parse as integer: " + value);
              } catch (IllegalAccessException | IllegalArgumentException
                      | ExceptionInInitializerError ex) {
                  throw new RuntimeException(ex);
              }
          }
  
          void setArray(int lineNumber, Record object, ArrayList<String> values,
                  int startIndex, int endIndex) throws ParseException {
              try {
                  final int[] array = new int[endIndex-startIndex];
                  for (int i=startIndex; i<endIndex; i++) {
                      final String value = values.get(startIndex+i);
                      try {
                          array[i] = Integer.parseInt(value.trim());
                      } catch (NumberFormatException ex) {
                          throw new ParseException(lineNumber, "can't parse field "
                                  + i + " as integer: " + value);
                      }
                  }
                  mField.set(object, array);
              } catch (IllegalAccessException | IllegalArgumentException
                      | ExceptionInInitializerError ex) {
                  throw new RuntimeException(ex);
              }
          }
      }
  
      /**
       * Sets long fields on Record objects using reflection.
       */
      private static class LongFieldSetter extends FieldSetter {
          LongFieldSetter(int index, int added, java.lang.reflect.Field field) {
              super(index, added, field);
          }
  
          void setField(int lineNumber, Record object, String value) throws ParseException {
              try {
                  mField.setLong(object, Long.parseLong(value.trim()));
              } catch (NumberFormatException ex) {
                  throw new ParseException(lineNumber, "can't parse as long: " + value);
              } catch (IllegalAccessException | IllegalArgumentException
                      | ExceptionInInitializerError ex) {
                  throw new RuntimeException(ex);
              }
          }
  
          void setArray(int lineNumber, Record object, ArrayList<String> values,
                  int startIndex, int endIndex) throws ParseException {
              try {
                  final long[] array = new long[endIndex-startIndex];
                  for (int i=0; i<(endIndex-startIndex); i++) {
                      final String value = values.get(startIndex+i);
                      try {
                          array[i] = Long.parseLong(value.trim());
                      } catch (NumberFormatException ex) {
                          throw new ParseException(lineNumber, "can't parse field "
                                  + i + " as long: " + value);
                      }
                  }
                  mField.set(object, array);
              } catch (IllegalAccessException | IllegalArgumentException
                      | ExceptionInInitializerError ex) {
                  throw new RuntimeException(ex);
              }
          }
      }
  
      /**
       * Sets String fields on Record objects using reflection.
       */
      private static class StringFieldSetter extends FieldSetter {
          StringFieldSetter(int index, int added, java.lang.reflect.Field field) {
              super(index, added, field);
          }
  
          void setField(int lineNumber, Record object, String value) throws ParseException {
              try {
                  mField.set(object, value);
              } catch (IllegalAccessException | IllegalArgumentException
                      | ExceptionInInitializerError ex) {
                  throw new RuntimeException(ex);
              }
          }
  
          void setArray(int lineNumber, Record object, ArrayList<String> values,
                  int startIndex, int endIndex) throws ParseException {
              try {
                  final String[] array = new String[endIndex-startIndex];
                  for (int i=0; i<(endIndex-startIndex); i++) {
                      array[i] = values.get(startIndex+1);
                  }
                  mField.set(object, array);
              } catch (IllegalAccessException | IllegalArgumentException
                      | ExceptionInInitializerError ex) {
                  throw new RuntimeException(ex);
              }
          }
      }
  
      /**
       * Sets enum fields on Record objects using reflection.
       *
       * To be parsed automatically, enums must have a public final String tag
       * field, which is the string that will appear in the csv for that enum value.
       */
      private static class EnumFieldSetter extends FieldSetter {
          private final HashMap<String,Enum> mTags = new HashMap<String,Enum>();
  
          EnumFieldSetter(int index, int added, java.lang.reflect.Field field) {
              super(index, added, field);
  
              // Build the mapping of tags to values.
              final Class<?> fieldType = field.getType();
  
              java.lang.reflect.Field tagField = null;
              try {
                  tagField = fieldType.getField("tag");
              } catch (NoSuchFieldException ex) {
                  throw new RuntimeException("Missing tag field."
                          + " To be parsed automatically, enums must have"
                          + " a String field called tag.  Enum class: " + fieldType.getName()
                          + " Containing class: " + field.getDeclaringClass().getName()
                          + " Field: " + field.getName());
  
              }
              if (!String.class.equals(tagField.getType())) {
                  throw new RuntimeException("Tag field is not string."
                          + " To be parsed automatically, enums must have"
                          + " a String field called tag.  Enum class: " + fieldType.getName()
                          + " Containing class: " + field.getDeclaringClass().getName()
                          + " Field: " + field.getName()
                          + " Tag field type: " + tagField.getType().getName());
              }
  
              for (final Object enumValue: fieldType.getEnumConstants()) {
                  String tag = null;
                  try {
                      tag = (String)tagField.get(enumValue);
                  } catch (IllegalAccessException | IllegalArgumentException
                          | ExceptionInInitializerError ex) {
                      throw new RuntimeException(ex);
                  }
                  mTags.put(tag, (Enum)enumValue);
              }
          }
  
          void setField(int lineNumber, Record object, String value) throws ParseException {
              final Enum enumValue = mTags.get(value);
              if (enumValue == null) {
                  throw new ParseException(lineNumber, "Could not find enum for field "
                          + getName() + " for tag: " + value);
              }
              try {
                  mField.set(object, enumValue);
              } catch (IllegalAccessException | IllegalArgumentException
                      | ExceptionInInitializerError ex) {
                  throw new RuntimeException(ex);
              }
          }
  
          void setArray(int lineNumber, Record object, ArrayList<String> values,
                  int startIndex, int endIndex) throws ParseException {
              try {
                  final Object array = Array.newInstance(mField.getType().getComponentType(),
                          endIndex-startIndex);
                  for (int i=0; i<(endIndex-startIndex); i++) {
                      final String value = values.get(startIndex+i);
                      final Enum enumValue = mTags.get(value);
                      if (enumValue == null) {
                          throw new ParseException(lineNumber, "Could not find enum for field "
                                  + getName() + " for tag: " + value);
                      }
                      Array.set(array, i, enumValue);
                  }
                  mField.set(object, array);
              } catch (IllegalAccessException | IllegalArgumentException
                      | ExceptionInInitializerError ex) {
                  throw new RuntimeException(ex);
              }
          }
      }
  
      /**
       * Factory for the record classes.  Uses reflection to create
       * the fields.
       */
      private static class RecordFactory {
          private String mTag;
          private Class<? extends Record> mSubclass;
          private ArrayList<FieldSetter> mFieldSetters;
  
          RecordFactory(String tag, Class<? extends Record> subclass,
                  ArrayList<FieldSetter> fieldSetters) {
              mTag = tag;
              mSubclass = subclass;
              mFieldSetters = fieldSetters;
          }
  
          /**
           * Create an object of one of the subclasses of Record, and fill
           * in the fields marked with the Field annotation.
           *
           * @return a new Record with the fields filled in. If there are missing
           *      fields, the {@link Record.complete} field will be set to false.
           */
          Record create(RawBatteryStats bs, int dumpsysVersion, int lineNumber,
                  ArrayList<String> fieldValues) {
              final boolean debug = false;
              Record record = null;
              try {
                  if (debug) {
                      System.err.println("Creating object: " + mSubclass.getSimpleName());
                  }
                  record = mSubclass.newInstance();
              } catch (IllegalAccessException | InstantiationException
                      | ExceptionInInitializerError | SecurityException ex) {
                  throw new RuntimeException("Exception creating " + mSubclass.getName()
                          + " for '" + mTag + "' record.", ex);
              }
              record.complete = true;
              int fieldIndex = 0;
              int setterIndex = 0;
              while (fieldIndex < fieldValues.size() && setterIndex < mFieldSetters.size()) {
                  final FieldSetter setter = mFieldSetters.get(setterIndex);
  
                  if (dumpsysVersion >= 0 && dumpsysVersion < setter.getAdded()) {
                      // The version being parsed doesn't have the field for this setter,
                      // so skip the setter but not the field.
                      setterIndex++;
                      continue;
                  }
  
                  final String value = fieldValues.get(fieldIndex);
                  try {
                      if (debug) {
                          System.err.println("   setting field " + setter + " to: " + value);
                      }
                      if (setter.isArray()) {
                          setter.setArray(lineNumber, record, fieldValues,
                                  fieldIndex, fieldValues.size());
                          // The rest of the fields have been consumed.
                          fieldIndex = fieldValues.size();
                          setterIndex = mFieldSetters.size();
                          break;
                      } else {
                          setter.setField(lineNumber, record, value);
                      }
                  } catch (ParseException ex) {
                      bs.addWarning(lineNumber, WarningId.BAD_FIELD_TYPE, fieldValues,
                              ex.getMessage(), mTag, value);
                      record.complete = false;
                  }
  
                  fieldIndex++;
                  setterIndex++;
              }
  
              // If there are extra fields, this record is complete, there are just
              // extra values, so we issue a warning but don't mark it incomplete.
              if (fieldIndex < fieldValues.size()) {
                  bs.addWarning(lineNumber, WarningId.TOO_MANY_FIELDS, fieldValues,
                          "Line '" + mTag + "' has extra fields.",
                          mTag, Integer.toString(fieldIndex), Integer.toString(fieldValues.size()));
                  if (debug) {
                      for (int i=0; i<mFieldSetters.size(); i++) {
                          System.err.println("    setter: [" + i + "] " + mFieldSetters.get(i));
                      }
                  }
              }
  
              // If we have any fields that are missing, add a warning and return null.
              for (; setterIndex < mFieldSetters.size(); setterIndex++) {
                  final FieldSetter setter = mFieldSetters.get(setterIndex);
                  if (dumpsysVersion >= 0 && dumpsysVersion >= setter.getAdded()) {
                      bs.addWarning(lineNumber, WarningId.NOT_ENOUGH_FIELDS, fieldValues,
                              "Line '" + mTag + "' missing field: index=" + setterIndex
                                  + " name=" + setter.getName(),
                              mTag, Integer.toString(setterIndex));
                      record.complete = false;
                  }
              }
  
              return record;
          }
      }
  
      /**
       * Parse the input stream and return a RawBatteryStats object.
       */
      public static RawBatteryStats parse(InputStream input) throws ParseException, IOException {
          final RawBatteryStats result = new RawBatteryStats();
          result.parseImpl(input);
          return result;
      }
  
      /**
       * Get a record.
       * <p>
       * If multiple of that record are found, returns the first one.  There will already
       * have been a warning recorded if the count annotation did not match what was in the
       * csv.
       * <p>
       * Returns null if there are no records of that type.
       */
      public <T extends Record> T getSingle(Class<T> cl) {
          final List<Record> list = mRecordsByType.get(cl.getName());
          if (list == null) {
              return null;
          }
          // Notes:
          //   - List can never be empty because the list itself wouldn't have been added.
          //   - Cast is safe because list was populated based on class name (let's assume
          //     there's only one class loader involved here).
          return (T)list.get(0);
      }
  
      /**
       * Get a record.
       * <p>
       * If multiple of that record are found, returns the first one that matches that uid.
       * <p>
       * Returns null if there are no records of that type that match the given uid.
       */
      public <T extends Record> T getSingle(Class<T> cl, int uid) {
          final List<Record> list = mRecordsByType.get(cl.getName());
          if (list == null) {
              return null;
          }
          for (final Record record: list) {
              if (record.uid == uid) {
                  // Cast is safe because list was populated based on class name (let's assume
                  // there's only one class loader involved here).
                  return (T)record;
              }
          }
          return null;
      }
  
      /**
       * Get all the records of the given type.
       */
      public <T extends Record> List<T> getMultiple(Class<T> cl) {
          final List<Record> list = mRecordsByType.get(cl.getName());
          if (list == null) {
              return ImmutableList.<T>of();
          }
          // Cast is safe because list was populated based on class name (let's assume
          // there's only one class loader involved here).
          return ImmutableList.copyOf((List<T>)list);
      }
  
      /**
       * Get the UIDs that are covered by this batterystats dump.
       */
      public Set<AttributionKey> getApps() {
          return mApps;
      }
  
      /**
       * No public constructor. Use {@link #parse}.
       */
      private RawBatteryStats() {
      }
  
      /**
       * Get the list of Record objects that were parsed from the csv.
       */
      public List<Record> getRecords() {
          return mRecords;
      }
  
      /**
       * Gets the warnings that were encountered during parsing.
       */
      public List<Warning> getWarnings() {
          return mWarnings;
      }
  
      /**
       * Implementation of the csv parsing.
       */
      private void parseImpl(InputStream input) throws ParseException, IOException {
          // Parse the csv
          CsvParser.parse(input, new CsvParser.LineProcessor() {
                      @Override
                      public void onLine(int lineNumber, ArrayList<String> fields)
                              throws ParseException {
                          handleCsvLine(lineNumber, fields);
                      }
                  });
  
          // Gather the records by class name for the getSingle() and getMultiple() functions.
          indexRecords();
  
          // Gather the uids from all the places UIDs come from, for getApps().
          indexApps();
      }
  
      /**
       * Handle a line of CSV input, creating the right Record object.
       */
      private void handleCsvLine(int lineNumber, ArrayList<String> fields) throws ParseException {
          // The standard rows all have the 4 core fields. Anything less isn't what we're
          // looking for.
          if (fields.size() <= 4) {
              addWarning(lineNumber, WarningId.TOO_FEW_FIELDS_FOR_LINE_TYPE, fields,
                      "Line with too few fields (" + fields.size() + ")",
                      Integer.toString(fields.size()));
              return;
          }
  
          final String lineType = fields.get(3);
  
          // Handle the vers line specially, because we need the version number
          // to make the rest of the machinery work.
          if ("vers".equals(lineType)) {
              final String versionText = fields.get(4);
              try {
                  mDumpsysVersion = Integer.parseInt(versionText);
              } catch (NumberFormatException ex) {
                  addWarning(lineNumber, WarningId.BAD_DUMPSYS_VERSION, fields,
                          "Couldn't parse dumpsys version number: '" + versionText,
                          versionText);
              }
          }
  
          // Find the right factory.
          final RecordFactory factory = sFactories.get(lineType);
          if (factory == null) {
              addWarning(lineNumber, WarningId.NO_MATCHING_LINE_TYPE, fields,
                      "No Record for line type '" + lineType + "'",
                      lineType);
              return;
          }
  
          // Create the record.
          final Record record = factory.create(this, mDumpsysVersion, lineNumber, fields);
          mRecords.add(record);
      }
  
      /**
       * Add to the list of warnings.
       */
      private void addWarning(int lineNumber, WarningId id,
              ArrayList<String> fields, String message, String... extras) {
          mWarnings.add(new Warning(lineNumber, id, fields, message, extras));
          final boolean debug = false;
          if (debug) {
              final StringBuilder text = new StringBuilder("line ");
              text.append(lineNumber);
              text.append(": WARNING: ");
              text.append(message);
              text.append("\n    fields: ");
              for (int i=0; i<fields.size(); i++) {
                  final String field = fields.get(i);
                  if (field.indexOf('"') >= 0) {
                      text.append('"');
                      text.append(field.replace("\"", "\"\""));
                      text.append('"');
                  } else {
                      text.append(field);
                  }
                  if (i != fields.size() - 1) {
                      text.append(',');
                  }
              }
              text.append('\n');
              for (String extra: extras) {
                  text.append("    extra: ");
                  text.append(extra);
                  text.append('\n');
              }
              System.err.print(text.toString());
          }
      }
  
      /**
       * Group records by class name.
       */
      private void indexRecords() {
          final HashMap<String,ArrayList<Record>> map = new HashMap<String,ArrayList<Record>>();
  
          // Iterate over all of the records
          for (Record record: mRecords) {
              final String className = record.getClass().getName();
  
              ArrayList<Record> list = map.get(className);
              if (list == null) {
                  list = new ArrayList<Record>();
                  map.put(className, list);
              }
  
              list.add(record);
          }
  
          // Make it immutable
          final HashMap<String,ImmutableList<Record>> result
                  = new HashMap<String,ImmutableList<Record>>();
          for (HashMap.Entry<String,ArrayList<Record>> entry: map.entrySet()) {
              result.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
          }
  
          // Initialize here so uninitialized access will result in NPE.
          mRecordsByType = ImmutableMap.copyOf(result);
      }
  
      /**
       * Collect the UIDs from the csv.
       *
       * They come from two places.
       * <ul>
       *   <li>The uid to package name map entries ({@link #Uid}) at the beginning.
       *   <li>The uid fields of the rest of the entries, some of which might not
       *       have package names associated with them.
       * </ul>
       *
       * TODO: Is this where we should also do the logic about the special UIDs?
       */
      private void indexApps() {
          final HashMap<Integer,HashSet<String>> uids = new HashMap<Integer,HashSet<String>>();
  
          // The Uid rows, from which we get package names
          for (Uid record: getMultiple(Uid.class)) {
              HashSet<String> list = uids.get(record.uidKey);
              if (list == null) {
                  list = new HashSet<String>();
                  uids.put(record.uidKey, list);
              }
              list.add(record.pkg);
          }
  
          // The uid fields of everything
          for (Record record: mRecords) {
              // The 0 in the INFO records isn't really root, it's just unfilled data.
              // The root uid (0) will show up practically in every record, but don't force it.
              if (record.category != Category.INFO) {
                  if (uids.get(record.uid) == null) {
                      // There is no other data about this UID, but it does exist!
                      uids.put(record.uid, new HashSet<String>());
                  }
              }
          }
  
          // Turn our temporary lists of package names into AttributionKeys.
          final HashSet<AttributionKey> result = new HashSet<AttributionKey>();
          for (HashMap.Entry<Integer,HashSet<String>> entry: uids.entrySet()) {
              result.add(new AttributionKey(entry.getKey(), entry.getValue()));
          }
  
          // Initialize here so uninitialized access will result in NPE.
          mApps = ImmutableSet.copyOf(result);
      }
  
      /**
       * Init the factory classes.
       */
      static {
          for (Class<?> cl: RawBatteryStats.class.getClasses()) {
              final Line lineAnnotation = cl.getAnnotation(Line.class);
              if (lineAnnotation != null && Record.class.isAssignableFrom(cl)) {
                  final ArrayList<FieldSetter> fieldSetters = new ArrayList<FieldSetter>();
  
                  for (java.lang.reflect.Field field: cl.getFields()) {
                      final Field fa = field.getAnnotation(Field.class);
                      if (fa != null) {
                          final Class<?> fieldType = field.getType();
                          final Class<?> innerType = fieldType.isArray()
                                  ? fieldType.getComponentType()
                                  : fieldType;
                          if (Integer.TYPE.equals(innerType)) {
                              fieldSetters.add(new IntFieldSetter(fa.index(), fa.added(), field));
                          } else if (Long.TYPE.equals(innerType)) {
                              fieldSetters.add(new LongFieldSetter(fa.index(), fa.added(), field));
                          } else if (String.class.equals(innerType)) {
                              fieldSetters.add(new StringFieldSetter(fa.index(), fa.added(), field));
                          } else if (innerType.isEnum()) {
                              fieldSetters.add(new EnumFieldSetter(fa.index(), fa.added(), field));
                          } else {
                              throw new RuntimeException("Unsupported field type '"
                                      + fieldType.getName() + "' on "
                                      + cl.getName() + "." + field.getName());
                          }
                      }
                  }
                  // Sort by index
                  Collections.sort(fieldSetters, new Comparator<FieldSetter>() {
                              @Override
                              public int compare(FieldSetter a, FieldSetter b) {
                                  return a.getIndex() - b.getIndex();
                              }
                          });
                  // Only the last one can be an array
                  for (int i=0; i<fieldSetters.size()-1; i++) {
                      if (fieldSetters.get(i).isArray()) {
                          throw new RuntimeException("Only the last (highest index) @Field"
                                  + " in class " + cl.getName() + " can be an array: "
                                  + fieldSetters.get(i).getName());
                      }
                  }
                  // Add to the map
                  sFactories.put(lineAnnotation.tag(), new RecordFactory(lineAnnotation.tag(),
                              (Class<Record>)cl, fieldSetters));
              }
          }
      }
  }
  
  