blob: 76c0482772f7bc82481005962e6100b4d527d929 [file] [log] [blame]
/*
* 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));
}
}
}
}