• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 com.android.powermodel;
18 
19 import java.io.InputStream;
20 import java.io.IOException;
21 import java.lang.annotation.ElementType;
22 import java.lang.annotation.Retention;
23 import java.lang.annotation.RetentionPolicy;
24 import java.lang.annotation.Target;
25 import java.lang.reflect.Array;
26 import java.lang.reflect.Field;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collections;
30 import java.util.Comparator;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Set;
36 import com.google.common.collect.ImmutableList;
37 import com.google.common.collect.ImmutableMap;
38 import com.google.common.collect.ImmutableSet;
39 
40 public class RawBatteryStats {
41     /**
42      * The factory objects for the records. Initialized in the static block.
43      */
44     private static HashMap<String,RecordFactory> sFactories
45             = new HashMap<String,RecordFactory>();
46 
47     /**
48      * The Record objects that have been parsed.
49      */
50     private ArrayList<Record> mRecords = new ArrayList<Record>();
51 
52     /**
53      * The Record objects that have been parsed, indexed by type.
54      *
55      * Don't use this before {@link #indexRecords()} has been called.
56      */
57     private ImmutableMap<String,ImmutableList<Record>> mRecordsByType;
58 
59     /**
60      * The attribution keys for which we have data (corresponding to UIDs we've seen).
61      * <p>
62      * Does not include the synthetic apps.
63      * <p>
64      * Don't use this before {@link #indexRecords()} has been called.
65      */
66     private ImmutableSet<AttributionKey> mApps;
67 
68     /**
69      * The warnings that have been issued during parsing.
70      */
71     private ArrayList<Warning> mWarnings = new ArrayList<Warning>();
72 
73     /**
74      * The version of the BatteryStats dumpsys that we are using.  This value
75      * is set to -1 initially, and then when parsing the (hopefully) first
76      * line, 'vers', it is set to the correct version.
77      */
78     private int mDumpsysVersion = -1;
79 
80     /**
81      * Enum used in the Line annotation to mark whether a field is expected to be
82      * system-wide or scoped to an app.
83      */
84     public enum Scope {
85         SYSTEM,
86         UID
87     }
88 
89     /**
90      * Enum used to indicated the expected number of results.
91      */
92     public enum Count {
93         SINGLE,
94         MULTIPLE
95     }
96 
97     /**
98      * Annotates classes that represent a line of CSV in the batterystats CSV
99      */
100     @Retention(RetentionPolicy.RUNTIME)
101     @Target(ElementType.TYPE)
102     @interface Line {
tag()103         String tag();
scope()104         Scope scope();
count()105         Count count();
106     }
107 
108     /**
109      * Annotates fields that should be parsed automatically from CSV
110      */
111     @Retention(RetentionPolicy.RUNTIME)
112     @Target(ElementType.FIELD)
113     @interface Field {
114         /**
115          * The "column" of this field in the most recent version of the CSV.
116          * When parsing old versions, fields that were added will be automatically
117          * removed and the indices will be fixed up.
118          *
119          * The header fields (version, uid, category, type) will be automatically
120          * handled for the base Line type.  The index 0 should start after those.
121          */
index()122         int index();
123 
124         /**
125          * First version that this field appears in.
126          */
added()127         int added() default 0;
128     }
129 
130     /**
131      * Each line in the BatteryStats CSV is tagged with a category, that says
132      * which of the time collection modes was used for the data.
133      */
134     public enum Category {
135         INFO("i"),
136         LAST("l"),
137         UNPLUGGED("u"),
138         CURRENT("c");
139 
140         public final String tag;
141 
Category(String tag)142         Category(String tag) {
143             this.tag = tag;
144         }
145     }
146 
147     /**
148      * Base class for all lines in a batterystats CSV file.
149      */
150     public static class Record {
151         /**
152          * Whether all of the fields for the indicated version of this record
153          * have been filled in.
154          */
155         public boolean complete;
156 
157 
158         @Field(index=-4)
159         public int lineVersion;
160 
161         @Field(index=-3)
162         public int uid;
163 
164         @Field(index=-2)
165         public Category category;
166 
167         @Field(index=-1)
168         public String lineType;
169     }
170 
171     @Line(tag="bt", scope=Scope.SYSTEM, count=Count.SINGLE)
172     public static class Battery extends Record {
173         // If which != STATS_SINCE_CHARGED, the csv will be "N/A" and we will get
174         // a parsing warning.  Nobody uses anything other than STATS_SINCE_CHARGED.
175         @Field(index=0)
176         public int startCount;
177 
178         @Field(index=1)
179         public long whichBatteryRealtimeMs;
180 
181         @Field(index=2)
182         public long whichBatteryUptimeMs;
183 
184         @Field(index=3)
185         public long totalRealtimeMs;
186 
187         @Field(index=4)
188         public long totalUptimeMs;
189 
190         @Field(index=5)
191         public long getStartClockTimeMs;
192 
193         @Field(index=6)
194         public long whichBatteryScreenOffRealtimeMs;
195 
196         @Field(index=7)
197         public long whichBatteryScreenOffUptimeMs;
198 
199         @Field(index=8)
200         public long estimatedBatteryCapacityMah;
201 
202         @Field(index=9)
203         public long minLearnedBatteryCapacityMah;
204 
205         @Field(index=10)
206         public long maxLearnedBatteryCapacityMah;
207 
208         @Field(index=11)
209         public long screenDozeTimeMs;
210     }
211 
212     @Line(tag="gn", scope=Scope.SYSTEM, count=Count.SINGLE)
213     public static class GlobalNetwork extends Record {
214         @Field(index=0)
215         public long mobileRxTotalBytes;
216 
217         @Field(index=1)
218         public long mobileTxTotalBytes;
219 
220         @Field(index=2)
221         public long wifiRxTotalBytes;
222 
223         @Field(index=3)
224         public long wifiTxTotalBytes;
225 
226         @Field(index=4)
227         public long mobileRxTotalPackets;
228 
229         @Field(index=5)
230         public long mobileTxTotalPackets;
231 
232         @Field(index=6)
233         public long wifiRxTotalPackets;
234 
235         @Field(index=7)
236         public long wifiTxTotalPackets;
237 
238         @Field(index=8)
239         public long btRxTotalBytes;
240 
241         @Field(index=9)
242         public long btTxTotalBytes;
243     }
244 
245     @Line(tag="gmcd", scope=Scope.SYSTEM, count=Count.SINGLE)
246     public static class GlobalModemController extends Record {
247         @Field(index=0)
248         public long idleMs;
249 
250         @Field(index=1)
251         public long rxTimeMs;
252 
253         @Field(index=2)
254         public long powerMaMs;
255 
256         @Field(index=3)
257         public long[] txTimeMs;
258     }
259 
260     @Line(tag="m", scope=Scope.SYSTEM, count=Count.SINGLE)
261     public static class Misc extends Record {
262         @Field(index=0)
263         public long screenOnTimeMs;
264 
265         @Field(index=1)
266         public long phoneOnTimeMs;
267 
268         @Field(index=2)
269         public long fullWakeLockTimeTotalMs;
270 
271         @Field(index=3)
272         public long partialWakeLockTimeTotalMs;
273 
274         @Field(index=4)
275         public long mobileRadioActiveTimeMs;
276 
277         @Field(index=5)
278         public long mobileRadioActiveAdjustedTimeMs;
279 
280         @Field(index=6)
281         public long interactiveTimeMs;
282 
283         @Field(index=7)
284         public long powerSaveModeEnabledTimeMs;
285 
286         @Field(index=8)
287         public int connectivityChangeCount;
288 
289         @Field(index=9)
290         public long deepDeviceIdleModeTimeMs;
291 
292         @Field(index=10)
293         public long deepDeviceIdleModeCount;
294 
295         @Field(index=11)
296         public long deepDeviceIdlingTimeMs;
297 
298         @Field(index=12)
299         public long deepDeviceIdlingCount;
300 
301         @Field(index=13)
302         public long mobileRadioActiveCount;
303 
304         @Field(index=14)
305         public long mobileRadioActiveUnknownTimeMs;
306 
307         @Field(index=15)
308         public long lightDeviceIdleModeTimeMs;
309 
310         @Field(index=16)
311         public long lightDeviceIdleModeCount;
312 
313         @Field(index=17)
314         public long lightDeviceIdlingTimeMs;
315 
316         @Field(index=18)
317         public long lightDeviceIdlingCount;
318 
319         @Field(index=19)
320         public long lightLongestDeviceIdleModeTimeMs;
321 
322         @Field(index=20)
323         public long deepLongestDeviceIdleModeTimeMs;
324     }
325 
326     @Line(tag="nt", scope=Scope.UID, count=Count.SINGLE)
327     public static class Network extends Record {
328         @Field(index=0)
329         public long mobileRxBytes;
330 
331         @Field(index=1)
332         public long mobileTxBytes;
333 
334         @Field(index=2)
335         public long wifiRxBytes;
336 
337         @Field(index=3)
338         public long wifiTxBytes;
339 
340         @Field(index=4)
341         public long mobileRxPackets;
342 
343         @Field(index=5)
344         public long mobileTxPackets;
345 
346         @Field(index=6)
347         public long wifiRxPackets;
348 
349         @Field(index=7)
350         public long wifiTxPackets;
351 
352         // This is microseconds, because... batterystats.
353         @Field(index=8)
354         public long mobileRadioActiveTimeUs;
355 
356         @Field(index=9)
357         public long mobileRadioActiveCount;
358 
359         @Field(index=10)
360         public long btRxBytes;
361 
362         @Field(index=11)
363         public long btTxBytes;
364 
365         @Field(index=12)
366         public long mobileWakeupCount;
367 
368         @Field(index=13)
369         public long wifiWakeupCount;
370 
371         @Field(index=14)
372         public long mobileBgRxBytes;
373 
374         @Field(index=15)
375         public long mobileBgTxBytes;
376 
377         @Field(index=16)
378         public long wifiBgRxBytes;
379 
380         @Field(index=17)
381         public long wifiBgTxBytes;
382 
383         @Field(index=18)
384         public long mobileBgRxPackets;
385 
386         @Field(index=19)
387         public long mobileBgTxPackets;
388 
389         @Field(index=20)
390         public long wifiBgRxPackets;
391 
392         @Field(index=21)
393         public long wifiBgTxPackets;
394     }
395 
396     @Line(tag="sgt", scope=Scope.SYSTEM, count=Count.SINGLE)
397     public static class SignalStrengthTime extends Record {
398         @Field(index=0)
399         public long[] phoneSignalStrengthTimeMs;
400     }
401 
402     @Line(tag="sst", scope=Scope.SYSTEM, count=Count.SINGLE)
403     public static class SignalScanningTime extends Record {
404         @Field(index=0)
405         public long phoneSignalScanningTimeMs;
406     }
407 
408     @Line(tag="uid", scope=Scope.UID, count=Count.MULTIPLE)
409     public static class Uid extends Record {
410         @Field(index=0)
411         public int uidKey;
412 
413         @Field(index=1)
414         public String pkg;
415     }
416 
417     @Line(tag="vers", scope=Scope.SYSTEM, count=Count.SINGLE)
418     public static class Version extends Record {
419         @Field(index=0)
420         public int dumpsysVersion;
421 
422         @Field(index=1)
423         public int parcelVersion;
424 
425         @Field(index=2)
426         public String startPlatformVersion;
427 
428         @Field(index=3)
429         public String endPlatformVersion;
430     }
431 
432     /**
433      * Codes for the warnings to classify warnings without parsing them.
434      */
435     public enum WarningId {
436         /**
437          * A row didn't have enough fields to match any records and let us extract
438          * a line type.
439          */
440         TOO_FEW_FIELDS_FOR_LINE_TYPE,
441 
442         /**
443          * We couldn't find a Record for the given line type.
444          */
445         NO_MATCHING_LINE_TYPE,
446 
447         /**
448          * Couldn't set the value of a field. Usually this is because the
449          * contents of a numeric type couldn't be parsed.
450          */
451         BAD_FIELD_TYPE,
452 
453         /**
454          * There were extra field values in the input text.
455          */
456         TOO_MANY_FIELDS,
457 
458         /**
459          * There were fields that we were expecting (for this version
460          * of the dumpsys) that weren't provided in the CSV data.
461          */
462         NOT_ENOUGH_FIELDS,
463 
464         /**
465          * The dumpsys version in the 'vers' CSV line couldn't be parsed.
466          */
467         BAD_DUMPSYS_VERSION
468     }
469 
470     /**
471      * A non-fatal problem detected during parsing.
472      */
473     public static class Warning {
474         private int mLineNumber;
475         private WarningId mId;
476         private ArrayList<String> mFields;
477         private String mMessage;
478         private String[] mExtras;
479 
Warning(int lineNumber, WarningId id, ArrayList<String> fields, String message, String[] extras)480         public Warning(int lineNumber, WarningId id, ArrayList<String> fields, String message,
481                 String[] extras) {
482             mLineNumber = lineNumber;
483             mId = id;
484             mFields = fields;
485             mMessage = message;
486             mExtras = extras;
487         }
488 
getLineNumber()489         public int getLineNumber() {
490             return mLineNumber;
491         }
492 
getFields()493         public ArrayList<String> getFields() {
494             return mFields;
495         }
496 
getMessage()497         public String getMessage() {
498             return mMessage;
499         }
500 
getExtras()501         public String[] getExtras() {
502             return mExtras;
503         }
504     }
505 
506     /**
507      * Base class for classes to set fields on Record objects via reflection.
508      */
509     private abstract static class FieldSetter {
510         private int mIndex;
511         private int mAdded;
512         protected java.lang.reflect.Field mField;
513 
FieldSetter(int index, int added, java.lang.reflect.Field field)514         FieldSetter(int index, int added, java.lang.reflect.Field field) {
515             mIndex = index;
516             mAdded = added;
517             mField = field;
518         }
519 
getName()520         String getName() {
521             return mField.getName();
522         }
523 
getIndex()524         int getIndex() {
525             return mIndex;
526         }
527 
getAdded()528         int getAdded() {
529             return mAdded;
530         }
531 
isArray()532         boolean isArray() {
533             return mField.getType().isArray();
534         }
535 
setField(int lineNumber, Record object, String value)536         abstract void setField(int lineNumber, Record object, String value) throws ParseException;
setArray(int lineNumber, Record object, ArrayList<String> values, int startIndex, int endIndex)537         abstract void setArray(int lineNumber, Record object, ArrayList<String> values,
538                 int startIndex, int endIndex) throws ParseException;
539 
540         @Override
toString()541         public String toString() {
542             final String className = getClass().getName();
543             int startIndex = Math.max(className.lastIndexOf('.'), className.lastIndexOf('$'));
544             if (startIndex < 0) {
545                 startIndex = 0;
546             } else {
547                 startIndex++;
548             }
549             return className.substring(startIndex) + "(index=" + mIndex + " added=" + mAdded
550                     + " field=" + mField.getName()
551                     + " type=" + mField.getType().getSimpleName()
552                     + ")";
553         }
554     }
555 
556     /**
557      * Sets int fields on Record objects using reflection.
558      */
559     private static class IntFieldSetter extends FieldSetter {
IntFieldSetter(int index, int added, java.lang.reflect.Field field)560         IntFieldSetter(int index, int added, java.lang.reflect.Field field) {
561             super(index, added, field);
562         }
563 
setField(int lineNumber, Record object, String value)564         void setField(int lineNumber, Record object, String value) throws ParseException {
565             try {
566                 mField.setInt(object, Integer.parseInt(value.trim()));
567             } catch (NumberFormatException ex) {
568                 throw new ParseException(lineNumber, "can't parse as integer: " + value);
569             } catch (IllegalAccessException | IllegalArgumentException
570                     | ExceptionInInitializerError ex) {
571                 throw new RuntimeException(ex);
572             }
573         }
574 
setArray(int lineNumber, Record object, ArrayList<String> values, int startIndex, int endIndex)575         void setArray(int lineNumber, Record object, ArrayList<String> values,
576                 int startIndex, int endIndex) throws ParseException {
577             try {
578                 final int[] array = new int[endIndex-startIndex];
579                 for (int i=startIndex; i<endIndex; i++) {
580                     final String value = values.get(startIndex+i);
581                     try {
582                         array[i] = Integer.parseInt(value.trim());
583                     } catch (NumberFormatException ex) {
584                         throw new ParseException(lineNumber, "can't parse field "
585                                 + i + " as integer: " + value);
586                     }
587                 }
588                 mField.set(object, array);
589             } catch (IllegalAccessException | IllegalArgumentException
590                     | ExceptionInInitializerError ex) {
591                 throw new RuntimeException(ex);
592             }
593         }
594     }
595 
596     /**
597      * Sets long fields on Record objects using reflection.
598      */
599     private static class LongFieldSetter extends FieldSetter {
LongFieldSetter(int index, int added, java.lang.reflect.Field field)600         LongFieldSetter(int index, int added, java.lang.reflect.Field field) {
601             super(index, added, field);
602         }
603 
setField(int lineNumber, Record object, String value)604         void setField(int lineNumber, Record object, String value) throws ParseException {
605             try {
606                 mField.setLong(object, Long.parseLong(value.trim()));
607             } catch (NumberFormatException ex) {
608                 throw new ParseException(lineNumber, "can't parse as long: " + value);
609             } catch (IllegalAccessException | IllegalArgumentException
610                     | ExceptionInInitializerError ex) {
611                 throw new RuntimeException(ex);
612             }
613         }
614 
setArray(int lineNumber, Record object, ArrayList<String> values, int startIndex, int endIndex)615         void setArray(int lineNumber, Record object, ArrayList<String> values,
616                 int startIndex, int endIndex) throws ParseException {
617             try {
618                 final long[] array = new long[endIndex-startIndex];
619                 for (int i=0; i<(endIndex-startIndex); i++) {
620                     final String value = values.get(startIndex+i);
621                     try {
622                         array[i] = Long.parseLong(value.trim());
623                     } catch (NumberFormatException ex) {
624                         throw new ParseException(lineNumber, "can't parse field "
625                                 + i + " as long: " + value);
626                     }
627                 }
628                 mField.set(object, array);
629             } catch (IllegalAccessException | IllegalArgumentException
630                     | ExceptionInInitializerError ex) {
631                 throw new RuntimeException(ex);
632             }
633         }
634     }
635 
636     /**
637      * Sets String fields on Record objects using reflection.
638      */
639     private static class StringFieldSetter extends FieldSetter {
StringFieldSetter(int index, int added, java.lang.reflect.Field field)640         StringFieldSetter(int index, int added, java.lang.reflect.Field field) {
641             super(index, added, field);
642         }
643 
setField(int lineNumber, Record object, String value)644         void setField(int lineNumber, Record object, String value) throws ParseException {
645             try {
646                 mField.set(object, value);
647             } catch (IllegalAccessException | IllegalArgumentException
648                     | ExceptionInInitializerError ex) {
649                 throw new RuntimeException(ex);
650             }
651         }
652 
setArray(int lineNumber, Record object, ArrayList<String> values, int startIndex, int endIndex)653         void setArray(int lineNumber, Record object, ArrayList<String> values,
654                 int startIndex, int endIndex) throws ParseException {
655             try {
656                 final String[] array = new String[endIndex-startIndex];
657                 for (int i=0; i<(endIndex-startIndex); i++) {
658                     array[i] = values.get(startIndex+1);
659                 }
660                 mField.set(object, array);
661             } catch (IllegalAccessException | IllegalArgumentException
662                     | ExceptionInInitializerError ex) {
663                 throw new RuntimeException(ex);
664             }
665         }
666     }
667 
668     /**
669      * Sets enum fields on Record objects using reflection.
670      *
671      * To be parsed automatically, enums must have a public final String tag
672      * field, which is the string that will appear in the csv for that enum value.
673      */
674     private static class EnumFieldSetter extends FieldSetter {
675         private final HashMap<String,Enum> mTags = new HashMap<String,Enum>();
676 
EnumFieldSetter(int index, int added, java.lang.reflect.Field field)677         EnumFieldSetter(int index, int added, java.lang.reflect.Field field) {
678             super(index, added, field);
679 
680             // Build the mapping of tags to values.
681             final Class<?> fieldType = field.getType();
682 
683             java.lang.reflect.Field tagField = null;
684             try {
685                 tagField = fieldType.getField("tag");
686             } catch (NoSuchFieldException ex) {
687                 throw new RuntimeException("Missing tag field."
688                         + " To be parsed automatically, enums must have"
689                         + " a String field called tag.  Enum class: " + fieldType.getName()
690                         + " Containing class: " + field.getDeclaringClass().getName()
691                         + " Field: " + field.getName());
692 
693             }
694             if (!String.class.equals(tagField.getType())) {
695                 throw new RuntimeException("Tag field is not string."
696                         + " To be parsed automatically, enums must have"
697                         + " a String field called tag.  Enum class: " + fieldType.getName()
698                         + " Containing class: " + field.getDeclaringClass().getName()
699                         + " Field: " + field.getName()
700                         + " Tag field type: " + tagField.getType().getName());
701             }
702 
703             for (final Object enumValue: fieldType.getEnumConstants()) {
704                 String tag = null;
705                 try {
706                     tag = (String)tagField.get(enumValue);
707                 } catch (IllegalAccessException | IllegalArgumentException
708                         | ExceptionInInitializerError ex) {
709                     throw new RuntimeException(ex);
710                 }
711                 mTags.put(tag, (Enum)enumValue);
712             }
713         }
714 
setField(int lineNumber, Record object, String value)715         void setField(int lineNumber, Record object, String value) throws ParseException {
716             final Enum enumValue = mTags.get(value);
717             if (enumValue == null) {
718                 throw new ParseException(lineNumber, "Could not find enum for field "
719                         + getName() + " for tag: " + value);
720             }
721             try {
722                 mField.set(object, enumValue);
723             } catch (IllegalAccessException | IllegalArgumentException
724                     | ExceptionInInitializerError ex) {
725                 throw new RuntimeException(ex);
726             }
727         }
728 
setArray(int lineNumber, Record object, ArrayList<String> values, int startIndex, int endIndex)729         void setArray(int lineNumber, Record object, ArrayList<String> values,
730                 int startIndex, int endIndex) throws ParseException {
731             try {
732                 final Object array = Array.newInstance(mField.getType().getComponentType(),
733                         endIndex-startIndex);
734                 for (int i=0; i<(endIndex-startIndex); i++) {
735                     final String value = values.get(startIndex+i);
736                     final Enum enumValue = mTags.get(value);
737                     if (enumValue == null) {
738                         throw new ParseException(lineNumber, "Could not find enum for field "
739                                 + getName() + " for tag: " + value);
740                     }
741                     Array.set(array, i, enumValue);
742                 }
743                 mField.set(object, array);
744             } catch (IllegalAccessException | IllegalArgumentException
745                     | ExceptionInInitializerError ex) {
746                 throw new RuntimeException(ex);
747             }
748         }
749     }
750 
751     /**
752      * Factory for the record classes.  Uses reflection to create
753      * the fields.
754      */
755     private static class RecordFactory {
756         private String mTag;
757         private Class<? extends Record> mSubclass;
758         private ArrayList<FieldSetter> mFieldSetters;
759 
RecordFactory(String tag, Class<? extends Record> subclass, ArrayList<FieldSetter> fieldSetters)760         RecordFactory(String tag, Class<? extends Record> subclass,
761                 ArrayList<FieldSetter> fieldSetters) {
762             mTag = tag;
763             mSubclass = subclass;
764             mFieldSetters = fieldSetters;
765         }
766 
767         /**
768          * Create an object of one of the subclasses of Record, and fill
769          * in the fields marked with the Field annotation.
770          *
771          * @return a new Record with the fields filled in. If there are missing
772          *      fields, the {@link Record.complete} field will be set to false.
773          */
create(RawBatteryStats bs, int dumpsysVersion, int lineNumber, ArrayList<String> fieldValues)774         Record create(RawBatteryStats bs, int dumpsysVersion, int lineNumber,
775                 ArrayList<String> fieldValues) {
776             final boolean debug = false;
777             Record record = null;
778             try {
779                 if (debug) {
780                     System.err.println("Creating object: " + mSubclass.getSimpleName());
781                 }
782                 record = mSubclass.newInstance();
783             } catch (IllegalAccessException | InstantiationException
784                     | ExceptionInInitializerError | SecurityException ex) {
785                 throw new RuntimeException("Exception creating " + mSubclass.getName()
786                         + " for '" + mTag + "' record.", ex);
787             }
788             record.complete = true;
789             int fieldIndex = 0;
790             int setterIndex = 0;
791             while (fieldIndex < fieldValues.size() && setterIndex < mFieldSetters.size()) {
792                 final FieldSetter setter = mFieldSetters.get(setterIndex);
793 
794                 if (dumpsysVersion >= 0 && dumpsysVersion < setter.getAdded()) {
795                     // The version being parsed doesn't have the field for this setter,
796                     // so skip the setter but not the field.
797                     setterIndex++;
798                     continue;
799                 }
800 
801                 final String value = fieldValues.get(fieldIndex);
802                 try {
803                     if (debug) {
804                         System.err.println("   setting field " + setter + " to: " + value);
805                     }
806                     if (setter.isArray()) {
807                         setter.setArray(lineNumber, record, fieldValues,
808                                 fieldIndex, fieldValues.size());
809                         // The rest of the fields have been consumed.
810                         fieldIndex = fieldValues.size();
811                         setterIndex = mFieldSetters.size();
812                         break;
813                     } else {
814                         setter.setField(lineNumber, record, value);
815                     }
816                 } catch (ParseException ex) {
817                     bs.addWarning(lineNumber, WarningId.BAD_FIELD_TYPE, fieldValues,
818                             ex.getMessage(), mTag, value);
819                     record.complete = false;
820                 }
821 
822                 fieldIndex++;
823                 setterIndex++;
824             }
825 
826             // If there are extra fields, this record is complete, there are just
827             // extra values, so we issue a warning but don't mark it incomplete.
828             if (fieldIndex < fieldValues.size()) {
829                 bs.addWarning(lineNumber, WarningId.TOO_MANY_FIELDS, fieldValues,
830                         "Line '" + mTag + "' has extra fields.",
831                         mTag, Integer.toString(fieldIndex), Integer.toString(fieldValues.size()));
832                 if (debug) {
833                     for (int i=0; i<mFieldSetters.size(); i++) {
834                         System.err.println("    setter: [" + i + "] " + mFieldSetters.get(i));
835                     }
836                 }
837             }
838 
839             // If we have any fields that are missing, add a warning and return null.
840             for (; setterIndex < mFieldSetters.size(); setterIndex++) {
841                 final FieldSetter setter = mFieldSetters.get(setterIndex);
842                 if (dumpsysVersion >= 0 && dumpsysVersion >= setter.getAdded()) {
843                     bs.addWarning(lineNumber, WarningId.NOT_ENOUGH_FIELDS, fieldValues,
844                             "Line '" + mTag + "' missing field: index=" + setterIndex
845                                 + " name=" + setter.getName(),
846                             mTag, Integer.toString(setterIndex));
847                     record.complete = false;
848                 }
849             }
850 
851             return record;
852         }
853     }
854 
855     /**
856      * Parse the input stream and return a RawBatteryStats object.
857      */
parse(InputStream input)858     public static RawBatteryStats parse(InputStream input) throws ParseException, IOException {
859         final RawBatteryStats result = new RawBatteryStats();
860         result.parseImpl(input);
861         return result;
862     }
863 
864     /**
865      * Get a record.
866      * <p>
867      * If multiple of that record are found, returns the first one.  There will already
868      * have been a warning recorded if the count annotation did not match what was in the
869      * csv.
870      * <p>
871      * Returns null if there are no records of that type.
872      */
getSingle(Class<T> cl)873     public <T extends Record> T getSingle(Class<T> cl) {
874         final List<Record> list = mRecordsByType.get(cl.getName());
875         if (list == null) {
876             return null;
877         }
878         // Notes:
879         //   - List can never be empty because the list itself wouldn't have been added.
880         //   - Cast is safe because list was populated based on class name (let's assume
881         //     there's only one class loader involved here).
882         return (T)list.get(0);
883     }
884 
885     /**
886      * Get a record.
887      * <p>
888      * If multiple of that record are found, returns the first one that matches that uid.
889      * <p>
890      * Returns null if there are no records of that type that match the given uid.
891      */
getSingle(Class<T> cl, int uid)892     public <T extends Record> T getSingle(Class<T> cl, int uid) {
893         final List<Record> list = mRecordsByType.get(cl.getName());
894         if (list == null) {
895             return null;
896         }
897         for (final Record record: list) {
898             if (record.uid == uid) {
899                 // Cast is safe because list was populated based on class name (let's assume
900                 // there's only one class loader involved here).
901                 return (T)record;
902             }
903         }
904         return null;
905     }
906 
907     /**
908      * Get all the records of the given type.
909      */
getMultiple(Class<T> cl)910     public <T extends Record> List<T> getMultiple(Class<T> cl) {
911         final List<Record> list = mRecordsByType.get(cl.getName());
912         if (list == null) {
913             return ImmutableList.<T>of();
914         }
915         // Cast is safe because list was populated based on class name (let's assume
916         // there's only one class loader involved here).
917         return ImmutableList.copyOf((List<T>)list);
918     }
919 
920     /**
921      * Get the UIDs that are covered by this batterystats dump.
922      */
getApps()923     public Set<AttributionKey> getApps() {
924         return mApps;
925     }
926 
927     /**
928      * No public constructor. Use {@link #parse}.
929      */
RawBatteryStats()930     private RawBatteryStats() {
931     }
932 
933     /**
934      * Get the list of Record objects that were parsed from the csv.
935      */
getRecords()936     public List<Record> getRecords() {
937         return mRecords;
938     }
939 
940     /**
941      * Gets the warnings that were encountered during parsing.
942      */
getWarnings()943     public List<Warning> getWarnings() {
944         return mWarnings;
945     }
946 
947     /**
948      * Implementation of the csv parsing.
949      */
parseImpl(InputStream input)950     private void parseImpl(InputStream input) throws ParseException, IOException {
951         // Parse the csv
952         CsvParser.parse(input, new CsvParser.LineProcessor() {
953                     @Override
954                     public void onLine(int lineNumber, ArrayList<String> fields)
955                             throws ParseException {
956                         handleCsvLine(lineNumber, fields);
957                     }
958                 });
959 
960         // Gather the records by class name for the getSingle() and getMultiple() functions.
961         indexRecords();
962 
963         // Gather the uids from all the places UIDs come from, for getApps().
964         indexApps();
965     }
966 
967     /**
968      * Handle a line of CSV input, creating the right Record object.
969      */
handleCsvLine(int lineNumber, ArrayList<String> fields)970     private void handleCsvLine(int lineNumber, ArrayList<String> fields) throws ParseException {
971         // The standard rows all have the 4 core fields. Anything less isn't what we're
972         // looking for.
973         if (fields.size() <= 4) {
974             addWarning(lineNumber, WarningId.TOO_FEW_FIELDS_FOR_LINE_TYPE, fields,
975                     "Line with too few fields (" + fields.size() + ")",
976                     Integer.toString(fields.size()));
977             return;
978         }
979 
980         final String lineType = fields.get(3);
981 
982         // Handle the vers line specially, because we need the version number
983         // to make the rest of the machinery work.
984         if ("vers".equals(lineType)) {
985             final String versionText = fields.get(4);
986             try {
987                 mDumpsysVersion = Integer.parseInt(versionText);
988             } catch (NumberFormatException ex) {
989                 addWarning(lineNumber, WarningId.BAD_DUMPSYS_VERSION, fields,
990                         "Couldn't parse dumpsys version number: '" + versionText,
991                         versionText);
992             }
993         }
994 
995         // Find the right factory.
996         final RecordFactory factory = sFactories.get(lineType);
997         if (factory == null) {
998             addWarning(lineNumber, WarningId.NO_MATCHING_LINE_TYPE, fields,
999                     "No Record for line type '" + lineType + "'",
1000                     lineType);
1001             return;
1002         }
1003 
1004         // Create the record.
1005         final Record record = factory.create(this, mDumpsysVersion, lineNumber, fields);
1006         mRecords.add(record);
1007     }
1008 
1009     /**
1010      * Add to the list of warnings.
1011      */
addWarning(int lineNumber, WarningId id, ArrayList<String> fields, String message, String... extras)1012     private void addWarning(int lineNumber, WarningId id,
1013             ArrayList<String> fields, String message, String... extras) {
1014         mWarnings.add(new Warning(lineNumber, id, fields, message, extras));
1015         final boolean debug = false;
1016         if (debug) {
1017             final StringBuilder text = new StringBuilder("line ");
1018             text.append(lineNumber);
1019             text.append(": WARNING: ");
1020             text.append(message);
1021             text.append("\n    fields: ");
1022             for (int i=0; i<fields.size(); i++) {
1023                 final String field = fields.get(i);
1024                 if (field.indexOf('"') >= 0) {
1025                     text.append('"');
1026                     text.append(field.replace("\"", "\"\""));
1027                     text.append('"');
1028                 } else {
1029                     text.append(field);
1030                 }
1031                 if (i != fields.size() - 1) {
1032                     text.append(',');
1033                 }
1034             }
1035             text.append('\n');
1036             for (String extra: extras) {
1037                 text.append("    extra: ");
1038                 text.append(extra);
1039                 text.append('\n');
1040             }
1041             System.err.print(text.toString());
1042         }
1043     }
1044 
1045     /**
1046      * Group records by class name.
1047      */
indexRecords()1048     private void indexRecords() {
1049         final HashMap<String,ArrayList<Record>> map = new HashMap<String,ArrayList<Record>>();
1050 
1051         // Iterate over all of the records
1052         for (Record record: mRecords) {
1053             final String className = record.getClass().getName();
1054 
1055             ArrayList<Record> list = map.get(className);
1056             if (list == null) {
1057                 list = new ArrayList<Record>();
1058                 map.put(className, list);
1059             }
1060 
1061             list.add(record);
1062         }
1063 
1064         // Make it immutable
1065         final HashMap<String,ImmutableList<Record>> result
1066                 = new HashMap<String,ImmutableList<Record>>();
1067         for (HashMap.Entry<String,ArrayList<Record>> entry: map.entrySet()) {
1068             result.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
1069         }
1070 
1071         // Initialize here so uninitialized access will result in NPE.
1072         mRecordsByType = ImmutableMap.copyOf(result);
1073     }
1074 
1075     /**
1076      * Collect the UIDs from the csv.
1077      *
1078      * They come from two places.
1079      * <ul>
1080      *   <li>The uid to package name map entries ({@link #Uid}) at the beginning.
1081      *   <li>The uid fields of the rest of the entries, some of which might not
1082      *       have package names associated with them.
1083      * </ul>
1084      *
1085      * TODO: Is this where we should also do the logic about the special UIDs?
1086      */
indexApps()1087     private void indexApps() {
1088         final HashMap<Integer,HashSet<String>> uids = new HashMap<Integer,HashSet<String>>();
1089 
1090         // The Uid rows, from which we get package names
1091         for (Uid record: getMultiple(Uid.class)) {
1092             HashSet<String> list = uids.get(record.uidKey);
1093             if (list == null) {
1094                 list = new HashSet<String>();
1095                 uids.put(record.uidKey, list);
1096             }
1097             list.add(record.pkg);
1098         }
1099 
1100         // The uid fields of everything
1101         for (Record record: mRecords) {
1102             // The 0 in the INFO records isn't really root, it's just unfilled data.
1103             // The root uid (0) will show up practically in every record, but don't force it.
1104             if (record.category != Category.INFO) {
1105                 if (uids.get(record.uid) == null) {
1106                     // There is no other data about this UID, but it does exist!
1107                     uids.put(record.uid, new HashSet<String>());
1108                 }
1109             }
1110         }
1111 
1112         // Turn our temporary lists of package names into AttributionKeys.
1113         final HashSet<AttributionKey> result = new HashSet<AttributionKey>();
1114         for (HashMap.Entry<Integer,HashSet<String>> entry: uids.entrySet()) {
1115             result.add(new AttributionKey(entry.getKey(), entry.getValue()));
1116         }
1117 
1118         // Initialize here so uninitialized access will result in NPE.
1119         mApps = ImmutableSet.copyOf(result);
1120     }
1121 
1122     /**
1123      * Init the factory classes.
1124      */
1125     static {
1126         for (Class<?> cl: RawBatteryStats.class.getClasses()) {
1127             final Line lineAnnotation = cl.getAnnotation(Line.class);
1128             if (lineAnnotation != null && Record.class.isAssignableFrom(cl)) {
1129                 final ArrayList<FieldSetter> fieldSetters = new ArrayList<FieldSetter>();
1130 
1131                 for (java.lang.reflect.Field field: cl.getFields()) {
1132                     final Field fa = field.getAnnotation(Field.class);
1133                     if (fa != null) {
1134                         final Class<?> fieldType = field.getType();
1135                         final Class<?> innerType = fieldType.isArray()
1136                                 ? fieldType.getComponentType()
1137                                 : fieldType;
1138                         if (Integer.TYPE.equals(innerType)) {
fieldSetters.add(new IntFieldSetter(fa.index(), fa.added(), field))1139                             fieldSetters.add(new IntFieldSetter(fa.index(), fa.added(), field));
1140                         } else if (Long.TYPE.equals(innerType)) {
fieldSetters.add(new LongFieldSetter(fa.index(), fa.added(), field))1141                             fieldSetters.add(new LongFieldSetter(fa.index(), fa.added(), field));
1142                         } else if (String.class.equals(innerType)) {
fieldSetters.add(new StringFieldSetter(fa.index(), fa.added(), field))1143                             fieldSetters.add(new StringFieldSetter(fa.index(), fa.added(), field));
1144                         } else if (innerType.isEnum()) {
fieldSetters.add(new EnumFieldSetter(fa.index(), fa.added(), field))1145                             fieldSetters.add(new EnumFieldSetter(fa.index(), fa.added(), field));
1146                         } else {
1147                             throw new RuntimeException("Unsupported field type '"
1148                                     + fieldType.getName() + "' on "
1149                                     + cl.getName() + "." + field.getName());
1150                         }
1151                     }
1152                 }
1153                 // Sort by index
Collections.sort(fieldSetters, new Comparator<FieldSetter>() { @Override public int compare(FieldSetter a, FieldSetter b) { return a.getIndex() - b.getIndex(); } })1154                 Collections.sort(fieldSetters, new Comparator<FieldSetter>() {
1155                             @Override
1156                             public int compare(FieldSetter a, FieldSetter b) {
1157                                 return a.getIndex() - b.getIndex();
1158                             }
1159                         });
1160                 // Only the last one can be an array
1161                 for (int i=0; i<fieldSetters.size()-1; i++) {
1162                     if (fieldSetters.get(i).isArray()) {
1163                         throw new RuntimeException("Only the last (highest index) @Field"
1164                                 + " in class " + cl.getName() + " can be an array: "
1165                                 + fieldSetters.get(i).getName());
1166                     }
1167                 }
1168                 // Add to the map
lineAnnotation.tag()1169                 sFactories.put(lineAnnotation.tag(), new RecordFactory(lineAnnotation.tag(),
1170                             (Class<Record>)cl, fieldSetters));
1171             }
1172         }
1173     }
1174 }
1175 
1176