1 /*
2  * Copyright (C) 2017 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 package android.cts.statsd.atom;
17 
18 import static android.cts.statsd.atom.DeviceAtomTestCase.DEVICE_SIDE_TEST_APK;
19 import static android.cts.statsd.atom.DeviceAtomTestCase.DEVICE_SIDE_TEST_PACKAGE;
20 import static com.google.common.truth.Truth.assertThat;
21 import static com.google.common.truth.Truth.assertWithMessage;
22 
23 import android.os.BatteryStatsProto;
24 import android.os.StatsDataDumpProto;
25 import android.service.battery.BatteryServiceDumpProto;
26 import android.service.batterystats.BatteryStatsServiceDumpProto;
27 import android.service.procstats.ProcessStatsServiceDumpProto;
28 import com.android.annotations.Nullable;
29 import com.android.internal.os.StatsdConfigProto.AtomMatcher;
30 import com.android.internal.os.StatsdConfigProto.EventMetric;
31 import com.android.internal.os.StatsdConfigProto.FieldFilter;
32 import com.android.internal.os.StatsdConfigProto.FieldMatcher;
33 import com.android.internal.os.StatsdConfigProto.FieldValueMatcher;
34 import com.android.internal.os.StatsdConfigProto.GaugeMetric;
35 import com.android.internal.os.StatsdConfigProto.Predicate;
36 import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
37 import com.android.internal.os.StatsdConfigProto.SimplePredicate;
38 import com.android.internal.os.StatsdConfigProto.StatsdConfig;
39 import com.android.internal.os.StatsdConfigProto.TimeUnit;
40 import com.android.os.AtomsProto.AppBreadcrumbReported;
41 import com.android.os.AtomsProto.Atom;
42 import com.android.os.AtomsProto.ProcessStatsPackageProto;
43 import com.android.os.AtomsProto.ProcessStatsProto;
44 import com.android.os.AtomsProto.ProcessStatsStateProto;
45 import com.android.os.StatsLog;
46 import com.android.os.StatsLog.ConfigMetricsReport;
47 import com.android.os.StatsLog.ConfigMetricsReportList;
48 import com.android.os.StatsLog.CountMetricData;
49 import com.android.os.StatsLog.DurationMetricData;
50 import com.android.os.StatsLog.EventMetricData;
51 import com.android.os.StatsLog.GaugeBucketInfo;
52 import com.android.os.StatsLog.GaugeMetricData;
53 import com.android.os.StatsLog.StatsLogReport;
54 import com.android.os.StatsLog.StatsLogReport.GaugeMetricDataWrapper;
55 import com.android.os.StatsLog.ValueMetricData;
56 import com.android.tradefed.device.DeviceNotAvailableException;
57 import com.android.tradefed.log.LogUtil;
58 import com.android.tradefed.util.CommandResult;
59 import com.android.tradefed.util.CommandStatus;
60 import com.android.tradefed.util.Pair;
61 import com.google.common.collect.Range;
62 import com.google.common.io.Files;
63 import com.google.protobuf.ByteString;
64 import java.io.File;
65 import java.text.SimpleDateFormat;
66 import java.util.ArrayList;
67 import java.util.Arrays;
68 import java.util.Collections;
69 import java.util.Comparator;
70 import java.util.Date;
71 import java.util.HashMap;
72 import java.util.LinkedList;
73 import java.util.List;
74 import java.util.Map;
75 import java.util.Queue;
76 import java.util.Random;
77 import java.util.Set;
78 import java.util.StringTokenizer;
79 import java.util.function.Function;
80 import java.util.regex.Matcher;
81 import java.util.regex.Pattern;
82 import java.util.stream.Collectors;
83 import perfetto.protos.PerfettoConfig.DataSourceConfig;
84 import perfetto.protos.PerfettoConfig.FtraceConfig;
85 import perfetto.protos.PerfettoConfig.TraceConfig;
86 
87 /**
88  * Base class for testing Statsd atoms.
89  * Validates reporting of statsd logging based on different events
90  */
91 public class AtomTestCase extends BaseTestCase {
92 
93     /**
94      * Run tests that are optional; they are not valid CTS tests per se, since not all devices can
95      * be expected to pass them, but can be run, if desired, to ensure they work when appropriate.
96      */
97     public static final boolean OPTIONAL_TESTS_ENABLED = false;
98 
99     public static final String UPDATE_CONFIG_CMD = "cmd stats config update";
100     public static final String DUMP_REPORT_CMD = "cmd stats dump-report";
101     public static final String DUMP_BATTERY_CMD = "dumpsys battery";
102     public static final String DUMP_BATTERYSTATS_CMD = "dumpsys batterystats";
103     public static final String DUMPSYS_STATS_CMD = "dumpsys stats";
104     public static final String DUMP_PROCSTATS_CMD = "dumpsys procstats";
105     public static final String REMOVE_CONFIG_CMD = "cmd stats config remove";
106     /** ID of the config, which evaluates to -1572883457. */
107     public static final long CONFIG_ID = "cts_config".hashCode();
108 
109     public static final String FEATURE_AUDIO_OUTPUT = "android.hardware.audio.output";
110     public static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive";
111     public static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth";
112     public static final String FEATURE_BLUETOOTH_LE = "android.hardware.bluetooth_le";
113     public static final String FEATURE_CAMERA = "android.hardware.camera";
114     public static final String FEATURE_CAMERA_FLASH = "android.hardware.camera.flash";
115     public static final String FEATURE_CAMERA_FRONT = "android.hardware.camera.front";
116     public static final String FEATURE_LEANBACK_ONLY = "android.software.leanback_only";
117     public static final String FEATURE_LOCATION_GPS = "android.hardware.location.gps";
118     public static final String FEATURE_PC = "android.hardware.type.pc";
119     public static final String FEATURE_PICTURE_IN_PICTURE = "android.software.picture_in_picture";
120     public static final String FEATURE_TELEPHONY = "android.hardware.telephony";
121     public static final String FEATURE_WATCH = "android.hardware.type.watch";
122     public static final String FEATURE_WIFI = "android.hardware.wifi";
123     public static final String FEATURE_INCREMENTAL_DELIVERY =
124             "android.software.incremental_delivery";
125 
126     // Telephony phone types
127     public static final int PHONE_TYPE_GSM = 1;
128     public static final int PHONE_TYPE_CDMA = 2;
129     public static final int PHONE_TYPE_CDMA_LTE = 6;
130 
131     protected static final int WAIT_TIME_SHORT = 500;
132     protected static final int WAIT_TIME_LONG = 2_000;
133 
134     protected static final long SCREEN_STATE_CHANGE_TIMEOUT = 4000;
135     protected static final long SCREEN_STATE_POLLING_INTERVAL = 500;
136 
137     protected static final long NS_PER_SEC = (long) 1E+9;
138 
139     @Override
140     protected void setUp() throws Exception {
141         super.setUp();
142 
143         // Uninstall to clear the history in case it's still on the device.
144         removeConfig(CONFIG_ID);
145         getReportList(); // Clears data.
146     }
147 
148     @Override
149     protected void tearDown() throws Exception {
150         removeConfig(CONFIG_ID);
151         getDevice().uninstallPackage(DEVICE_SIDE_TEST_PACKAGE);
152         super.tearDown();
153     }
154 
155     /**
156      * Determines whether logcat indicates that incidentd fired since the given device date.
157      */
158     protected boolean didIncidentdFireSince(String date) throws Exception {
159         final String INCIDENTD_TAG = "incidentd";
160         final String INCIDENTD_STARTED_STRING = "reportIncident";
161         // TODO: Do something more robust than this in case of delayed logging.
162         Thread.sleep(1000);
163         String log = getLogcatSince(date, String.format(
164                 "-s %s -e %s", INCIDENTD_TAG, INCIDENTD_STARTED_STRING));
165         return log.contains(INCIDENTD_STARTED_STRING);
166     }
167 
168     protected boolean checkDeviceFor(String methodName) throws Exception {
169         try {
170             installPackage(DEVICE_SIDE_TEST_APK, true);
171             runDeviceTests(DEVICE_SIDE_TEST_PACKAGE, ".Checkers", methodName);
172             // Test passes, meaning that the answer is true.
173             LogUtil.CLog.d(methodName + "() indicates true.");
174             return true;
175         } catch (AssertionError e) {
176             // Method is designed to fail if the answer is false.
177             LogUtil.CLog.d(methodName + "() indicates false.");
178             return false;
179         }
180     }
181 
182     /**
183      * Returns a protobuf-encoded perfetto config that enables the kernel
184      * ftrace tracer with sched_switch for 10 seconds.
185      */
186     protected ByteString getPerfettoConfig() {
187         TraceConfig.Builder builder = TraceConfig.newBuilder();
188 
189         TraceConfig.BufferConfig buffer = TraceConfig.BufferConfig
190             .newBuilder()
191             .setSizeKb(128)
192             .build();
193         builder.addBuffers(buffer);
194 
195         FtraceConfig ftraceConfig = FtraceConfig.newBuilder()
196             .addFtraceEvents("sched/sched_switch")
197             .build();
198         DataSourceConfig dataSourceConfig = DataSourceConfig.newBuilder()
199             .setName("linux.ftrace")
200             .setTargetBuffer(0)
201             .setFtraceConfig(ftraceConfig)
202             .build();
203         TraceConfig.DataSource dataSource = TraceConfig.DataSource
204             .newBuilder()
205             .setConfig(dataSourceConfig)
206             .build();
207         builder.addDataSources(dataSource);
208 
209         builder.setDurationMs(10000);
210         builder.setAllowUserBuildTracing(true);
211 
212         TraceConfig.IncidentReportConfig incident = TraceConfig.IncidentReportConfig
213             .newBuilder()
214             .setDestinationPackage("foo.bar.baz")
215             .build();
216         builder.setIncidentReportConfig(incident);
217 
218         // To avoid being hit with guardrails firing in multiple test runs back
219         // to back, we set a unique session key for each config.
220         Random random = new Random();
221         StringBuilder sessionNameBuilder = new StringBuilder("statsd-cts-");
222         sessionNameBuilder.append(random.nextInt() & Integer.MAX_VALUE);
223         builder.setUniqueSessionName(sessionNameBuilder.toString());
224 
225         return builder.build().toByteString();
226     }
227 
228     /**
229      * Resets the state of the Perfetto guardrails. This avoids that the test fails if it's
230      * run too close of for too many times and hits the upload limit.
231      */
232     protected void resetPerfettoGuardrails() throws Exception {
233         final String cmd = "perfetto --reset-guardrails";
234         CommandResult cr = getDevice().executeShellV2Command(cmd);
235         if (cr.getStatus() != CommandStatus.SUCCESS)
236             throw new Exception(String.format("Error while executing %s: %s %s", cmd, cr.getStdout(), cr.getStderr()));
237     }
238 
239     private String probe(String path) throws Exception {
240         return getDevice().executeShellCommand("if [ -e " + path + " ] ; then"
241                 + " cat " + path + " ; else echo -1 ; fi");
242     }
243 
244     /**
245      * Determines whether perfetto enabled the kernel ftrace tracer.
246      */
247     protected boolean isSystemTracingEnabled() throws Exception {
248         final String traceFsPath = "/sys/kernel/tracing/tracing_on";
249         String tracing_on = probe(traceFsPath);
250         if (tracing_on.startsWith("0")) return false;
251         if (tracing_on.startsWith("1")) return true;
252 
253         // fallback to debugfs
254         LogUtil.CLog.d("Unexpected state for %s = %s. Falling back to debugfs", traceFsPath,
255                 tracing_on);
256 
257         final String debugFsPath = "/sys/kernel/debug/tracing/tracing_on";
258         tracing_on = probe(debugFsPath);
259         if (tracing_on.startsWith("0")) return false;
260         if (tracing_on.startsWith("1")) return true;
261         throw new Exception(String.format("Unexpected state for %s = %s", traceFsPath, tracing_on));
262     }
263 
264     protected static StatsdConfig.Builder createConfigBuilder() {
265       return StatsdConfig.newBuilder()
266           .setId(CONFIG_ID)
267           .addAllowedLogSource("AID_SYSTEM")
268           .addAllowedLogSource("AID_BLUETOOTH")
269           // TODO(b/134091167): Fix bluetooth source name issue in Auto platform.
270           .addAllowedLogSource("com.android.bluetooth")
271           .addAllowedLogSource("AID_LMKD")
272           .addAllowedLogSource("AID_RADIO")
273           .addAllowedLogSource("AID_ROOT")
274           .addAllowedLogSource("AID_STATSD")
275           .addAllowedLogSource("com.android.systemui")
276           .addAllowedLogSource(DeviceAtomTestCase.DEVICE_SIDE_TEST_PACKAGE)
277           .addDefaultPullPackages("AID_RADIO")
278           .addDefaultPullPackages("AID_SYSTEM")
279           .addWhitelistedAtomIds(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER);
280     }
281 
282     protected void createAndUploadConfig(int atomTag) throws Exception {
283         StatsdConfig.Builder conf = createConfigBuilder();
284         addAtomEvent(conf, atomTag);
285         uploadConfig(conf);
286     }
287 
288     protected void uploadConfig(StatsdConfig.Builder config) throws Exception {
289         uploadConfig(config.build());
290     }
291 
292     protected void uploadConfig(StatsdConfig config) throws Exception {
293         LogUtil.CLog.d("Uploading the following config:\n" + config.toString());
294         File configFile = File.createTempFile("statsdconfig", ".config");
295         configFile.deleteOnExit();
296         Files.write(config.toByteArray(), configFile);
297         String remotePath = "/data/local/tmp/" + configFile.getName();
298         getDevice().pushFile(configFile, remotePath);
299         getDevice().executeShellCommand(
300                 String.join(" ", "cat", remotePath, "|", UPDATE_CONFIG_CMD,
301                         String.valueOf(CONFIG_ID)));
302         getDevice().executeShellCommand("rm " + remotePath);
303     }
304 
305     protected void removeConfig(long configId) throws Exception {
306         getDevice().executeShellCommand(
307                 String.join(" ", REMOVE_CONFIG_CMD, String.valueOf(configId)));
308     }
309 
310     /** Gets the statsd report and sorts it. Note that this also deletes that report from statsd. */
311     protected List<EventMetricData> getEventMetricDataList() throws Exception {
312         ConfigMetricsReportList reportList = getReportList();
313         return getEventMetricDataList(reportList);
314     }
315 
316     /**
317      *  Gets a List of sorted ConfigMetricsReports from ConfigMetricsReportList.
318      */
319     protected List<ConfigMetricsReport> getSortedConfigMetricsReports(
320             ConfigMetricsReportList configMetricsReportList) {
321         return configMetricsReportList.getReportsList().stream()
322                 .sorted(Comparator.comparing(ConfigMetricsReport::getCurrentReportWallClockNanos))
323                 .collect(Collectors.toList());
324     }
325 
326     /**
327      * Extracts and sorts the EventMetricData from the given ConfigMetricsReportList (which must
328      * contain a single report).
329      */
330     protected List<EventMetricData> getEventMetricDataList(ConfigMetricsReportList reportList)
331             throws Exception {
332         assertThat(reportList.getReportsCount()).isEqualTo(1);
333         ConfigMetricsReport report = reportList.getReports(0);
334 
335         List<EventMetricData> data = new ArrayList<>();
336         for (StatsLogReport metric : report.getMetricsList()) {
337           for (EventMetricData metricData :
338                metric.getEventMetrics().getDataList()) {
339             if (metricData.hasAtom()) {
340               data.add(metricData);
341             } else {
342               data.addAll(backfillAggregatedAtomsInEventMetric(metricData));
343             }
344           }
345         }
346         data.sort(Comparator.comparing(EventMetricData::getElapsedTimestampNanos));
347 
348         LogUtil.CLog.d("Get EventMetricDataList as following:\n");
349         for (EventMetricData d : data) {
350             LogUtil.CLog.d("Atom at " + d.getElapsedTimestampNanos() + ":\n" + d.getAtom().toString());
351         }
352         return data;
353     }
354 
355     protected List<Atom> getGaugeMetricDataList() throws Exception {
356         return getGaugeMetricDataList(/*checkTimestampTruncated=*/false);
357     }
358 
359     protected List<Atom> getGaugeMetricDataList(boolean checkTimestampTruncated) throws Exception {
360         ConfigMetricsReportList reportList = getReportList();
361         assertThat(reportList.getReportsCount()).isEqualTo(1);
362 
363         // only config
364         ConfigMetricsReport report = reportList.getReports(0);
365         assertThat(report.getMetricsCount()).isEqualTo(1);
366 
367         List<Atom> data = new ArrayList<>();
368         for (GaugeMetricData gaugeMetricData :
369                 report.getMetrics(0).getGaugeMetrics().getDataList()) {
370             assertThat(gaugeMetricData.getBucketInfoCount()).isEqualTo(1);
371             GaugeBucketInfo bucketInfo = gaugeMetricData.getBucketInfo(0);
372             if (bucketInfo.getAtomCount() != 0) {
373                 for (Atom atom : bucketInfo.getAtomList()) {
374                     data.add(atom);
375                 }
376             } else {
377                 data.addAll(backFillGaugeBucketAtoms(bucketInfo.getAggregatedAtomInfoList()));
378             }
379             if (checkTimestampTruncated) {
380                 for (long timestampNs : bucketInfo.getElapsedTimestampNanosList()) {
381                     assertTimestampIsTruncated(timestampNs);
382                 }
383             }
384         }
385 
386         LogUtil.CLog.d("Get GaugeMetricDataList as following:\n");
387         for (Atom d : data) {
388             LogUtil.CLog.d("Atom:\n" + d.toString());
389         }
390         return data;
391     }
392 
393     private List<Atom> backFillGaugeBucketAtoms(
394             List<StatsLog.AggregatedAtomInfo> atomInfoList) {
395         List<Pair<Atom, Long>> atomTimestamp = new ArrayList<>();
396         for (StatsLog.AggregatedAtomInfo atomInfo : atomInfoList) {
397             for (long timestampNs : atomInfo.getElapsedTimestampNanosList()) {
398                 atomTimestamp.add(Pair.create(atomInfo.getAtom(), timestampNs));
399             }
400         }
401         atomTimestamp.sort(Comparator.comparing(o -> o.second));
402         return atomTimestamp.stream().map(p -> p.first).collect(Collectors.toList());
403     }
404 
405     protected GaugeMetricDataWrapper backfillGaugeMetricData(GaugeMetricDataWrapper dataWrapper) {
406         GaugeMetricDataWrapper.Builder dataWrapperBuilder = dataWrapper.toBuilder();
407         List<GaugeMetricData> backfilledMetricData = new ArrayList<>();
408         for (GaugeMetricData gaugeMetricData : dataWrapperBuilder.getDataList()) {
409             GaugeMetricData.Builder gaugeMetricDataBuilder = gaugeMetricData.toBuilder();
410             List<GaugeBucketInfo> backfilledBuckets = new ArrayList<>();
411             for (GaugeBucketInfo bucketInfo : gaugeMetricData.getBucketInfoList()) {
412                 backfilledBuckets.add(backfillGaugeBucket(bucketInfo.toBuilder()));
413             }
414             gaugeMetricDataBuilder.clearBucketInfo();
415             gaugeMetricDataBuilder.addAllBucketInfo(backfilledBuckets);
416             backfilledMetricData.add(gaugeMetricDataBuilder.build());
417         }
418         dataWrapperBuilder.clearData();
419         dataWrapperBuilder.addAllData(backfilledMetricData);
420         return dataWrapperBuilder.build();
421     }
422 
423     private GaugeBucketInfo backfillGaugeBucket(GaugeBucketInfo.Builder bucketInfoBuilder) {
424         if (bucketInfoBuilder.getAtomCount() != 0) {
425             return bucketInfoBuilder.build();
426         }
427         List<Pair<Atom, Long>> atomTimestampData = new ArrayList<>();
428         for (StatsLog.AggregatedAtomInfo atomInfo : bucketInfoBuilder.getAggregatedAtomInfoList()) {
429             for (long timestampNs : atomInfo.getElapsedTimestampNanosList()) {
430                 atomTimestampData.add(Pair.create(atomInfo.getAtom(), timestampNs));
431             }
432         }
433         atomTimestampData.sort(Comparator.comparing(o -> o.second));
434         bucketInfoBuilder.clearAggregatedAtomInfo();
435         for (Pair<Atom, Long> atomTimestamp : atomTimestampData) {
436             bucketInfoBuilder.addAtom(atomTimestamp.first);
437             bucketInfoBuilder.addElapsedTimestampNanos(atomTimestamp.second);
438         }
439         return bucketInfoBuilder.build();
440     }
441 
442     /**
443      * Gets the statsd report and extract duration metric data.
444      * Note that this also deletes that report from statsd.
445      */
446     protected List<DurationMetricData> getDurationMetricDataList() throws Exception {
447         ConfigMetricsReportList reportList = getReportList();
448         assertThat(reportList.getReportsCount()).isEqualTo(1);
449         ConfigMetricsReport report = reportList.getReports(0);
450 
451         List<DurationMetricData> data = new ArrayList<>();
452         for (StatsLogReport metric : report.getMetricsList()) {
453             data.addAll(metric.getDurationMetrics().getDataList());
454         }
455 
456         LogUtil.CLog.d("Got DurationMetricDataList as following:\n");
457         for (DurationMetricData d : data) {
458             LogUtil.CLog.d("Duration " + d);
459         }
460         return data;
461     }
462 
463     /**
464      * Gets the statsd report and extract count metric data.
465      * Note that this also deletes that report from statsd.
466      */
467     protected List<CountMetricData> getCountMetricDataList() throws Exception {
468         ConfigMetricsReportList reportList = getReportList();
469         assertThat(reportList.getReportsCount()).isEqualTo(1);
470         ConfigMetricsReport report = reportList.getReports(0);
471 
472         List<CountMetricData> data = new ArrayList<>();
473         for (StatsLogReport metric : report.getMetricsList()) {
474             data.addAll(metric.getCountMetrics().getDataList());
475         }
476 
477         LogUtil.CLog.d("Got CountMetricDataList as following:\n");
478         for (CountMetricData d : data) {
479             LogUtil.CLog.d("Count " + d);
480         }
481         return data;
482     }
483 
484     /**
485      * Gets the statsd report and extract value metric data.
486      * Note that this also deletes that report from statsd.
487      */
488     protected List<ValueMetricData> getValueMetricDataList() throws Exception {
489         ConfigMetricsReportList reportList = getReportList();
490         assertThat(reportList.getReportsCount()).isEqualTo(1);
491         ConfigMetricsReport report = reportList.getReports(0);
492 
493         List<ValueMetricData> data = new ArrayList<>();
494         for (StatsLogReport metric : report.getMetricsList()) {
495             data.addAll(metric.getValueMetrics().getDataList());
496         }
497 
498         LogUtil.CLog.d("Got ValueMetricDataList as following:\n");
499         for (ValueMetricData d : data) {
500             LogUtil.CLog.d("Value " + d);
501         }
502         return data;
503     }
504 
505     protected StatsLogReport getStatsLogReport() throws Exception {
506         ConfigMetricsReport report = getConfigMetricsReport();
507         assertThat(report.hasUidMap()).isTrue();
508         assertThat(report.getMetricsCount()).isEqualTo(1);
509         return report.getMetrics(0);
510     }
511 
512     protected ConfigMetricsReport getConfigMetricsReport() throws Exception {
513         ConfigMetricsReportList reportList = getReportList();
514         assertThat(reportList.getReportsCount()).isEqualTo(1);
515         return reportList.getReports(0);
516     }
517 
518     /** Gets the statsd report. Note that this also deletes that report from statsd. */
519     protected ConfigMetricsReportList getReportList() throws Exception {
520         try {
521             ConfigMetricsReportList reportList = getDump(ConfigMetricsReportList.parser(),
522                     String.join(" ", DUMP_REPORT_CMD, String.valueOf(CONFIG_ID),
523                             "--include_current_bucket", "--proto"));
524             return reportList;
525         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
526             LogUtil.CLog.e("Failed to fetch and parse the statsd output report. "
527                     + "Perhaps there is not a valid statsd config for the requested "
528                     + "uid=" + getHostUid() + ", id=" + CONFIG_ID + ".");
529             throw (e);
530         }
531     }
532 
533     protected BatteryStatsProto getBatteryStatsProto() throws Exception {
534         try {
535             BatteryStatsProto batteryStatsProto = getDump(BatteryStatsServiceDumpProto.parser(),
536                     String.join(" ", DUMP_BATTERYSTATS_CMD,
537                             "--proto")).getBatterystats();
538             LogUtil.CLog.d("Got batterystats:\n " + batteryStatsProto.toString());
539             return batteryStatsProto;
540         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
541             LogUtil.CLog.e("Failed to dump batterystats proto");
542             throw (e);
543         }
544     }
545 
546     /** Gets reports from the statsd data incident section from the stats dumpsys. */
547     protected List<ConfigMetricsReportList> getReportsFromStatsDataDumpProto() throws Exception {
548         try {
549             StatsDataDumpProto statsProto = getDump(StatsDataDumpProto.parser(),
550                     String.join(" ", DUMPSYS_STATS_CMD, "--proto"));
551             // statsProto holds repeated bytes, which we must parse into ConfigMetricsReportLists.
552             List<ConfigMetricsReportList> reports
553                     = new ArrayList<>(statsProto.getConfigMetricsReportListCount());
554             for (ByteString reportListBytes : statsProto.getConfigMetricsReportListList()) {
555                 reports.add(ConfigMetricsReportList.parseFrom(reportListBytes));
556             }
557             LogUtil.CLog.d("Got dumpsys stats output:\n " + reports.toString());
558             return reports;
559         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
560             LogUtil.CLog.e("Failed to dumpsys stats proto");
561             throw (e);
562         }
563     }
564 
565     protected List<ProcessStatsProto> getProcStatsProto() throws Exception {
566         try {
567 
568             List<ProcessStatsProto> processStatsProtoList =
569                 new ArrayList<ProcessStatsProto>();
570             android.service.procstats.ProcessStatsSectionProto sectionProto = getDump(
571                     ProcessStatsServiceDumpProto.parser(),
572                     String.join(" ", DUMP_PROCSTATS_CMD,
573                             "--proto")).getProcstatsNow();
574             for (android.service.procstats.ProcessStatsProto stats :
575                     sectionProto.getProcessStatsList()) {
576                 ProcessStatsProto procStats = ProcessStatsProto.parser().parseFrom(
577                     stats.toByteArray());
578                 processStatsProtoList.add(procStats);
579             }
580             LogUtil.CLog.d("Got procstats:\n ");
581             for (ProcessStatsProto processStatsProto : processStatsProtoList) {
582                 LogUtil.CLog.d(processStatsProto.toString());
583             }
584             return processStatsProtoList;
585         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
586             LogUtil.CLog.e("Failed to dump procstats proto");
587             throw (e);
588         }
589     }
590 
591     /*
592      * Get all procstats package data in proto
593      */
594     protected List<ProcessStatsPackageProto> getAllProcStatsProto() throws Exception {
595         try {
596             android.service.procstats.ProcessStatsSectionProto sectionProto = getDump(
597                     ProcessStatsServiceDumpProto.parser(),
598                     String.join(" ", DUMP_PROCSTATS_CMD,
599                             "--proto")).getProcstatsOver24Hrs();
600             List<ProcessStatsPackageProto> processStatsProtoList =
601                 new ArrayList<ProcessStatsPackageProto>();
602             for (android.service.procstats.ProcessStatsPackageProto pkgStast :
603                 sectionProto.getPackageStatsList()) {
604               ProcessStatsPackageProto pkgAtom =
605                   ProcessStatsPackageProto.parser().parseFrom(pkgStast.toByteArray());
606                 processStatsProtoList.add(pkgAtom);
607             }
608             LogUtil.CLog.d("Got procstats:\n ");
609             for (ProcessStatsPackageProto processStatsProto : processStatsProtoList) {
610                 LogUtil.CLog.d(processStatsProto.toString());
611             }
612             return processStatsProtoList;
613         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
614             LogUtil.CLog.e("Failed to dump procstats proto");
615             throw (e);
616         }
617     }
618 
619     /*
620      * Get all processes' procstats statsd data in proto
621      */
622     protected List<android.service.procstats.ProcessStatsProto> getAllProcStatsProtoForStatsd()
623             throws Exception {
624         try {
625             android.service.procstats.ProcessStatsSectionProto sectionProto = getDump(
626                     android.service.procstats.ProcessStatsSectionProto.parser(),
627                     String.join(" ", DUMP_PROCSTATS_CMD,
628                             "--statsd"));
629             List<android.service.procstats.ProcessStatsProto> processStatsProtoList
630                     = sectionProto.getProcessStatsList();
631             LogUtil.CLog.d("Got procstats:\n ");
632             for (android.service.procstats.ProcessStatsProto processStatsProto
633                     : processStatsProtoList) {
634                 LogUtil.CLog.d(processStatsProto.toString());
635             }
636             return processStatsProtoList;
637         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
638             LogUtil.CLog.e("Failed to dump procstats proto");
639             throw (e);
640         }
641     }
642 
643     protected boolean hasBattery() throws Exception {
644         try {
645             BatteryServiceDumpProto batteryProto = getDump(BatteryServiceDumpProto.parser(),
646                     String.join(" ", DUMP_BATTERY_CMD, "--proto"));
647             LogUtil.CLog.d("Got battery service dump:\n " + batteryProto.toString());
648             return batteryProto.getIsPresent();
649         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
650             LogUtil.CLog.e("Failed to dump batteryservice proto");
651             throw (e);
652         }
653     }
654 
655     /** Creates a FieldValueMatcher.Builder corresponding to the given field. */
656     protected static FieldValueMatcher.Builder createFvm(int field) {
657         return FieldValueMatcher.newBuilder().setField(field);
658     }
659 
660     protected void addAtomEvent(StatsdConfig.Builder conf, int atomTag) throws Exception {
661         addAtomEvent(conf, atomTag, new ArrayList<FieldValueMatcher.Builder>());
662     }
663 
664     /**
665      * Adds an event to the config for an atom that matches the given key.
666      *
667      * @param conf    configuration
668      * @param atomTag atom tag (from atoms.proto)
669      * @param fvm     FieldValueMatcher.Builder for the relevant key
670      */
671     protected void addAtomEvent(StatsdConfig.Builder conf, int atomTag,
672             FieldValueMatcher.Builder fvm)
673             throws Exception {
674         addAtomEvent(conf, atomTag, Arrays.asList(fvm));
675     }
676 
677     /**
678      * Adds an event to the config for an atom that matches the given keys.
679      *
680      * @param conf   configuration
681      * @param atomId atom tag (from atoms.proto)
682      * @param fvms   list of FieldValueMatcher.Builders to attach to the atom. May be null.
683      */
684     protected void addAtomEvent(StatsdConfig.Builder conf, int atomId,
685             List<FieldValueMatcher.Builder> fvms) throws Exception {
686 
687         final String atomName = "Atom" + System.nanoTime();
688         final String eventName = "Event" + System.nanoTime();
689 
690         SimpleAtomMatcher.Builder sam = SimpleAtomMatcher.newBuilder().setAtomId(atomId);
691         if (fvms != null) {
692             for (FieldValueMatcher.Builder fvm : fvms) {
693                 sam.addFieldValueMatcher(fvm);
694             }
695         }
696         conf.addAtomMatcher(AtomMatcher.newBuilder()
697                 .setId(atomName.hashCode())
698                 .setSimpleAtomMatcher(sam));
699         conf.addEventMetric(EventMetric.newBuilder()
700                 .setId(eventName.hashCode())
701                 .setWhat(atomName.hashCode()));
702     }
703 
704     /**
705      * Adds an atom to a gauge metric of a config
706      *
707      * @param conf        configuration
708      * @param atomId      atom id (from atoms.proto)
709      * @param gaugeMetric the gauge metric to add
710      */
711     protected void addGaugeAtom(StatsdConfig.Builder conf, int atomId,
712             GaugeMetric.Builder gaugeMetric) throws Exception {
713         final String atomName = "Atom" + System.nanoTime();
714         final String gaugeName = "Gauge" + System.nanoTime();
715         final String predicateName = "APP_BREADCRUMB";
716         SimpleAtomMatcher.Builder sam = SimpleAtomMatcher.newBuilder().setAtomId(atomId);
717         conf.addAtomMatcher(AtomMatcher.newBuilder()
718                 .setId(atomName.hashCode())
719                 .setSimpleAtomMatcher(sam));
720         final String predicateTrueName = "APP_BREADCRUMB_1";
721         final String predicateFalseName = "APP_BREADCRUMB_2";
722         conf.addAtomMatcher(AtomMatcher.newBuilder()
723                 .setId(predicateTrueName.hashCode())
724                 .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
725                         .setAtomId(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER)
726                         .addFieldValueMatcher(FieldValueMatcher.newBuilder()
727                                 .setField(AppBreadcrumbReported.LABEL_FIELD_NUMBER)
728                                 .setEqInt(1)
729                         )
730                 )
731         )
732                 // Used to trigger predicate
733                 .addAtomMatcher(AtomMatcher.newBuilder()
734                         .setId(predicateFalseName.hashCode())
735                         .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
736                                 .setAtomId(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER)
737                                 .addFieldValueMatcher(FieldValueMatcher.newBuilder()
738                                         .setField(AppBreadcrumbReported.LABEL_FIELD_NUMBER)
739                                         .setEqInt(2)
740                                 )
741                         )
742                 );
743         conf.addPredicate(Predicate.newBuilder()
744                 .setId(predicateName.hashCode())
745                 .setSimplePredicate(SimplePredicate.newBuilder()
746                         .setStart(predicateTrueName.hashCode())
747                         .setStop(predicateFalseName.hashCode())
748                         .setCountNesting(false)
749                 )
750         );
751         gaugeMetric
752                 .setId(gaugeName.hashCode())
753                 .setWhat(atomName.hashCode())
754                 .setCondition(predicateName.hashCode());
755         conf.addGaugeMetric(gaugeMetric.build());
756     }
757 
758     /**
759      * Adds an atom to a gauge metric of a config
760      *
761      * @param conf      configuration
762      * @param atomId    atom id (from atoms.proto)
763      * @param dimension dimension is needed for most pulled atoms
764      */
765     protected void addGaugeAtomWithDimensions(StatsdConfig.Builder conf, int atomId,
766             @Nullable FieldMatcher.Builder dimension) throws Exception {
767         GaugeMetric.Builder gaugeMetric = GaugeMetric.newBuilder()
768                 .setGaugeFieldsFilter(FieldFilter.newBuilder().setIncludeAll(true).build())
769                 .setSamplingType(GaugeMetric.SamplingType.CONDITION_CHANGE_TO_TRUE)
770                 .setMaxNumGaugeAtomsPerBucket(10000)
771                 .setBucket(TimeUnit.CTS);
772         if (dimension != null) {
773             gaugeMetric.setDimensionsInWhat(dimension.build());
774         }
775         addGaugeAtom(conf, atomId, gaugeMetric);
776     }
777 
778     /**
779      * Asserts that each set of states in stateSets occurs at least once in data.
780      * Asserts that the states in data occur in the same order as the sets in stateSets.
781      *
782      * @param stateSets        A list of set of states, where each set represents an equivalent
783      *                         state of the device for the purpose of CTS.
784      * @param data             list of EventMetricData from statsd, produced by
785      *                         getReportMetricListData()
786      * @param wait             expected duration (in ms) between state changes; asserts that the
787      *                         actual wait
788      *                         time was wait/2 <= actual_wait <= 5*wait. Use 0 to ignore this
789      *                         assertion.
790      * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
791      */
792     public void assertStatesOccurred(List<Set<Integer>> stateSets, List<EventMetricData> data,
793             int wait, Function<Atom, Integer> getStateFromAtom) {
794         // Sometimes, there are more events than there are states.
795         // Eg: When the screen turns off, it may go into OFF and then DOZE immediately.
796         assertWithMessage("Too few states found").that(data.size()).isAtLeast(stateSets.size());
797         int stateSetIndex = 0; // Tracks which state set we expect the data to be in.
798         for (int dataIndex = 0; dataIndex < data.size(); dataIndex++) {
799             Atom atom = data.get(dataIndex).getAtom();
800             int state = getStateFromAtom.apply(atom);
801             // If state is in the current state set, we do not assert anything.
802             // If it is not, we expect to have transitioned to the next state set.
803             if (stateSets.get(stateSetIndex).contains(state)) {
804                 // No need to assert anything. Just log it.
805                 LogUtil.CLog.i("The following atom at dataIndex=" + dataIndex + " is "
806                         + "in stateSetIndex " + stateSetIndex + ":\n"
807                         + data.get(dataIndex).getAtom().toString());
808             } else {
809                 stateSetIndex += 1;
810                 LogUtil.CLog.i("Assert that the following atom at dataIndex=" + dataIndex + " is"
811                         + " in stateSetIndex " + stateSetIndex + ":\n"
812                         + data.get(dataIndex).getAtom().toString());
813                 assertWithMessage("Missed first state").that(dataIndex).isNotEqualTo(0);
814                 assertWithMessage("Too many states").that(stateSetIndex)
815                     .isLessThan(stateSets.size());
816                 assertWithMessage(String.format("Is in wrong state (%d)", state))
817                     .that(stateSets.get(stateSetIndex)).contains(state);
818                 if (wait > 0) {
819                     assertTimeDiffBetween(data.get(dataIndex - 1), data.get(dataIndex),
820                             wait / 2, wait * 5);
821                 }
822             }
823         }
824         assertWithMessage("Too few states").that(stateSetIndex).isEqualTo(stateSets.size() - 1);
825     }
826 
827     /**
828      * Removes all elements from data prior to the first occurrence of an element of state. After
829      * this method is called, the first element of data (if non-empty) is guaranteed to be an
830      * element in state.
831      *
832      * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
833      */
834     public void popUntilFind(List<EventMetricData> data, Set<Integer> state,
835             Function<Atom, Integer> getStateFromAtom) {
836         int firstStateIdx;
837         for (firstStateIdx = 0; firstStateIdx < data.size(); firstStateIdx++) {
838             Atom atom = data.get(firstStateIdx).getAtom();
839             if (state.contains(getStateFromAtom.apply(atom))) {
840                 break;
841             }
842         }
843         if (firstStateIdx == 0) {
844             // First first element already is in state, so there's nothing to do.
845             return;
846         }
847         data.subList(0, firstStateIdx).clear();
848     }
849 
850     /**
851      * Removes all elements from data after to the last occurrence of an element of state. After
852      * this method is called, the last element of data (if non-empty) is guaranteed to be an
853      * element in state.
854      *
855      * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
856      */
857     public void popUntilFindFromEnd(List<EventMetricData> data, Set<Integer> state,
858         Function<Atom, Integer> getStateFromAtom) {
859         int lastStateIdx;
860         for (lastStateIdx = data.size() - 1; lastStateIdx >= 0; lastStateIdx--) {
861             Atom atom = data.get(lastStateIdx).getAtom();
862             if (state.contains(getStateFromAtom.apply(atom))) {
863                 break;
864             }
865         }
866         if (lastStateIdx == data.size()-1) {
867             // Last element already is in state, so there's nothing to do.
868             return;
869         }
870         data.subList(lastStateIdx+1, data.size()).clear();
871     }
872 
873     /** Returns the UID of the host, which should always either be SHELL (2000) or ROOT (0). */
874     protected int getHostUid() throws DeviceNotAvailableException {
875         String strUid = "";
876         try {
877             strUid = getDevice().executeShellCommand("id -u");
878             return Integer.parseInt(strUid.trim());
879         } catch (NumberFormatException e) {
880             LogUtil.CLog.e("Failed to get host's uid via shell command. Found " + strUid);
881             // Fall back to alternative method...
882             if (getDevice().isAdbRoot()) {
883                 return 0; // ROOT
884             } else {
885                 return 2000; // SHELL
886             }
887         }
888     }
889 
890     protected String getProperty(String prop) throws Exception {
891         return getDevice().executeShellCommand("getprop " + prop).replace("\n", "");
892     }
893 
894     protected void turnScreenOn() throws Exception {
895         getDevice().executeShellCommand("input keyevent KEYCODE_WAKEUP");
896         getDevice().executeShellCommand("wm dismiss-keyguard");
897     }
898 
899     protected void turnScreenOff() throws Exception {
900         getDevice().executeShellCommand("input keyevent KEYCODE_SLEEP");
901     }
902 
903     protected void setChargingState(int state) throws Exception {
904         getDevice().executeShellCommand("cmd battery set status " + state);
905     }
906 
907     protected void unplugDevice() throws Exception {
908         // On batteryless devices on Android P or above, the 'unplug' command
909         // alone does not simulate the really unplugged state.
910         //
911         // This is because charging state is left as "unknown". Unless a valid
912         // state like 3 = BatteryManager.BATTERY_STATUS_DISCHARGING is set,
913         // framework does not consider the device as running on battery.
914         setChargingState(3);
915 
916         getDevice().executeShellCommand("cmd battery unplug");
917     }
918 
919     protected void plugInAc() throws Exception {
920         getDevice().executeShellCommand("cmd battery set ac 1");
921     }
922 
923     protected void plugInUsb() throws Exception {
924         getDevice().executeShellCommand("cmd battery set usb 1");
925     }
926 
927     protected void plugInWireless() throws Exception {
928         getDevice().executeShellCommand("cmd battery set wireless 1");
929     }
930 
931     protected void enableLooperStats() throws Exception {
932         getDevice().executeShellCommand("cmd looper_stats enable");
933     }
934 
935     protected void resetLooperStats() throws Exception {
936         getDevice().executeShellCommand("cmd looper_stats reset");
937     }
938 
939     protected void disableLooperStats() throws Exception {
940         getDevice().executeShellCommand("cmd looper_stats disable");
941     }
942 
943     protected void enableBinderStats() throws Exception {
944         getDevice().executeShellCommand("dumpsys binder_calls_stats --enable");
945     }
946 
947     protected void resetBinderStats() throws Exception {
948         getDevice().executeShellCommand("dumpsys binder_calls_stats --reset");
949     }
950 
951     protected void disableBinderStats() throws Exception {
952         getDevice().executeShellCommand("dumpsys binder_calls_stats --disable");
953     }
954 
955     protected void binderStatsNoSampling() throws Exception {
956         getDevice().executeShellCommand("dumpsys binder_calls_stats --no-sampling");
957     }
958 
959     protected void setUpLooperStats() throws Exception {
960         getDevice().executeShellCommand("cmd looper_stats enable");
961         getDevice().executeShellCommand("cmd looper_stats sampling_interval 1");
962         getDevice().executeShellCommand("cmd looper_stats reset");
963     }
964 
965     protected void cleanUpLooperStats() throws Exception {
966         getDevice().executeShellCommand("cmd looper_stats disable");
967     }
968 
969     public void setAppBreadcrumbPredicate() throws Exception {
970         doAppBreadcrumbReportedStart(1);
971     }
972 
973     public void clearAppBreadcrumbPredicate() throws Exception {
974         doAppBreadcrumbReportedStart(2);
975     }
976 
977     public void doAppBreadcrumbReportedStart(int label) throws Exception {
978         doAppBreadcrumbReported(label, AppBreadcrumbReported.State.START.ordinal());
979     }
980 
981     public void doAppBreadcrumbReportedStop(int label) throws Exception {
982         doAppBreadcrumbReported(label, AppBreadcrumbReported.State.STOP.ordinal());
983     }
984 
985     public void doAppBreadcrumbReported(int label) throws Exception {
986         doAppBreadcrumbReported(label, AppBreadcrumbReported.State.UNSPECIFIED.ordinal());
987     }
988 
989     public void doAppBreadcrumbReported(int label, int state) throws Exception {
990         getDevice().executeShellCommand(String.format(
991                 "cmd stats log-app-breadcrumb %d %d", label, state));
992     }
993 
994     protected void setBatteryLevel(int level) throws Exception {
995         getDevice().executeShellCommand("cmd battery set level " + level);
996     }
997 
998     protected void resetBatteryStatus() throws Exception {
999         getDevice().executeShellCommand("cmd battery reset");
1000     }
1001 
1002     protected int getScreenBrightness() throws Exception {
1003         return Integer.parseInt(
1004                 getDevice().executeShellCommand("settings get system screen_brightness").trim());
1005     }
1006 
1007     protected void setScreenBrightness(int brightness) throws Exception {
1008         getDevice().executeShellCommand("settings put system screen_brightness " + brightness);
1009     }
1010 
1011     // Gets whether "Always on Display" setting is enabled.
1012     // In rare cases, this is different from whether the device can enter SCREEN_STATE_DOZE.
1013     protected String getAodState() throws Exception {
1014         return getDevice().executeShellCommand("settings get secure doze_always_on");
1015     }
1016 
1017     protected void setAodState(String state) throws Exception {
1018         getDevice().executeShellCommand("settings put secure doze_always_on " + state);
1019     }
1020 
1021     protected boolean isScreenBrightnessModeManual() throws Exception {
1022         String mode = getDevice().executeShellCommand("settings get system screen_brightness_mode");
1023         return Integer.parseInt(mode.trim()) == 0;
1024     }
1025 
1026     protected void setScreenBrightnessMode(boolean manual) throws Exception {
1027         getDevice().executeShellCommand(
1028                 "settings put system screen_brightness_mode " + (manual ? 0 : 1));
1029     }
1030 
1031     protected void enterDozeModeLight() throws Exception {
1032         getDevice().executeShellCommand("dumpsys deviceidle force-idle light");
1033     }
1034 
1035     protected void enterDozeModeDeep() throws Exception {
1036         getDevice().executeShellCommand("dumpsys deviceidle force-idle deep");
1037     }
1038 
1039     protected void leaveDozeMode() throws Exception {
1040         getDevice().executeShellCommand("dumpsys deviceidle unforce");
1041         getDevice().executeShellCommand("dumpsys deviceidle disable");
1042         getDevice().executeShellCommand("dumpsys deviceidle enable");
1043     }
1044 
1045     protected void turnBatterySaverOn() throws Exception {
1046         unplugDevice();
1047         getDevice().executeShellCommand("settings put global low_power 1");
1048     }
1049 
1050     protected void turnBatterySaverOff() throws Exception {
1051         getDevice().executeShellCommand("settings put global low_power 0");
1052         getDevice().executeShellCommand("cmd battery reset");
1053     }
1054 
1055     protected void turnBatteryStatsAutoResetOn() throws Exception {
1056         getDevice().executeShellCommand("dumpsys batterystats enable no-auto-reset");
1057     }
1058 
1059     protected void turnBatteryStatsAutoResetOff() throws Exception {
1060         getDevice().executeShellCommand("dumpsys batterystats enable no-auto-reset");
1061     }
1062 
1063     protected void flushBatteryStatsHandlers() throws Exception {
1064         // Dumping batterystats will flush everything in the batterystats handler threads.
1065         getDevice().executeShellCommand(DUMP_BATTERYSTATS_CMD);
1066     }
1067 
1068     protected void rebootDevice() throws Exception {
1069         getDevice().rebootUntilOnline();
1070     }
1071 
1072     /**
1073      * Asserts that the two events are within the specified range of each other.
1074      *
1075      * @param d0        the event that should occur first
1076      * @param d1        the event that should occur second
1077      * @param minDiffMs d0 should precede d1 by at least this amount
1078      * @param maxDiffMs d0 should precede d1 by at most this amount
1079      */
1080     public static void assertTimeDiffBetween(EventMetricData d0, EventMetricData d1,
1081             int minDiffMs, int maxDiffMs) {
1082         long diffMs = (d1.getElapsedTimestampNanos() - d0.getElapsedTimestampNanos()) / 1_000_000;
1083         assertWithMessage("Illegal time difference")
1084             .that(diffMs).isIn(Range.closed((long) minDiffMs, (long) maxDiffMs));
1085     }
1086 
1087     protected String getCurrentLogcatDate() throws Exception {
1088         // TODO: Do something more robust than this for getting logcat markers.
1089         long timestampMs = getDevice().getDeviceDate();
1090         return new SimpleDateFormat("MM-dd HH:mm:ss.SSS")
1091                 .format(new Date(timestampMs));
1092     }
1093 
1094     protected String getLogcatSince(String date, String logcatParams) throws Exception {
1095         return getDevice().executeShellCommand(String.format(
1096                 "logcat -v threadtime -t '%s' -d %s", date, logcatParams));
1097     }
1098 
1099     // TODO: Remove this and migrate all usages to createConfigBuilder()
1100     protected StatsdConfig.Builder getPulledConfig() {
1101         return createConfigBuilder();
1102     }
1103     /**
1104      * Determines if the device has the given feature.
1105      * Prints a warning if its value differs from requiredAnswer.
1106      */
1107     protected boolean hasFeature(String featureName, boolean requiredAnswer) throws Exception {
1108         final String features = getDevice().executeShellCommand("pm list features");
1109         StringTokenizer featureToken = new StringTokenizer(features, "\n");
1110         boolean hasIt = false;
1111 
1112         while (featureToken.hasMoreTokens()) {
1113             if (("feature:" + featureName).equals(featureToken.nextToken())) {
1114                  hasIt = true;
1115                  break;
1116             }
1117         }
1118 
1119         if (hasIt != requiredAnswer) {
1120             LogUtil.CLog.w("Device does " + (requiredAnswer ? "not " : "") + "have feature "
1121                     + featureName);
1122         }
1123         return hasIt == requiredAnswer;
1124     }
1125 
1126     /**
1127      * Determines if the device has |file|.
1128      */
1129     protected boolean doesFileExist(String file) throws Exception {
1130         return getDevice().doesFileExist(file);
1131     }
1132 
1133     protected void turnOnAirplaneMode() throws Exception {
1134         getDevice().executeShellCommand("cmd connectivity airplane-mode enable");
1135     }
1136 
1137     protected void turnOffAirplaneMode() throws Exception {
1138         getDevice().executeShellCommand("cmd connectivity airplane-mode disable");
1139     }
1140 
1141     /**
1142      * Returns a list of fields and values for {@code className} from {@link TelephonyDebugService}
1143      * output.
1144      *
1145      * <p>Telephony dumpsys output does not support proto at the moment. This method provides
1146      * limited support for parsing its output. Specifically, it does not support arrays or
1147      * multi-line values.
1148      */
1149     private List<Map<String, String>> getTelephonyDumpEntries(String className) throws Exception {
1150         // Matches any line with indentation, except for lines with only spaces
1151         Pattern indentPattern = Pattern.compile("^(\\s*)[^ ].*$");
1152         // Matches pattern for class, e.g. "    Phone:"
1153         Pattern classNamePattern = Pattern.compile("^(\\s*)" + Pattern.quote(className) + ":.*$");
1154         // Matches pattern for key-value pairs, e.g. "     mPhoneId=1"
1155         Pattern keyValuePattern = Pattern.compile("^(\\s*)([a-zA-Z]+[a-zA-Z0-9_]*)\\=(.+)$");
1156         String response =
1157                 getDevice().executeShellCommand("dumpsys activity service TelephonyDebugService");
1158         Queue<String> responseLines = new LinkedList<>(Arrays.asList(response.split("[\\r\\n]+")));
1159 
1160         List<Map<String, String>> results = new ArrayList<>();
1161         while (responseLines.peek() != null) {
1162             Matcher matcher = classNamePattern.matcher(responseLines.poll());
1163             if (matcher.matches()) {
1164                 final int classIndentLevel = matcher.group(1).length();
1165                 final Map<String, String> instanceEntries = new HashMap<>();
1166                 while (responseLines.peek() != null) {
1167                     // Skip blank lines
1168                     matcher = indentPattern.matcher(responseLines.peek());
1169                     if (responseLines.peek().length() == 0 || !matcher.matches()) {
1170                         responseLines.poll();
1171                         continue;
1172                     }
1173                     // Finish (without consuming the line) if already parsed past this instance
1174                     final int indentLevel = matcher.group(1).length();
1175                     if (indentLevel <= classIndentLevel) {
1176                         break;
1177                     }
1178                     // Parse key-value pair if it belongs to the instance directly
1179                     matcher = keyValuePattern.matcher(responseLines.poll());
1180                     if (indentLevel == classIndentLevel + 1 && matcher.matches()) {
1181                         instanceEntries.put(matcher.group(2), matcher.group(3));
1182                     }
1183                 }
1184                 results.add(instanceEntries);
1185             }
1186         }
1187         return results;
1188     }
1189 
1190     protected int getActiveSimSlotCount() throws Exception {
1191         List<Map<String, String>> slots = getTelephonyDumpEntries("UiccSlot");
1192         long count = slots.stream().filter(slot -> "true".equals(slot.get("mActive"))).count();
1193         return Math.toIntExact(count);
1194     }
1195 
1196     /**
1197      * Returns the upper bound of active SIM profile count.
1198      *
1199      * <p>The value is an upper bound as eSIMs without profiles are also counted in.
1200      */
1201     protected int getActiveSimCountUpperBound() throws Exception {
1202         List<Map<String, String>> slots = getTelephonyDumpEntries("UiccSlot");
1203         long count = slots.stream().filter(slot ->
1204                 "true".equals(slot.get("mActive"))
1205                 && "CARDSTATE_PRESENT".equals(slot.get("mCardState"))).count();
1206         return Math.toIntExact(count);
1207     }
1208 
1209     /**
1210      * Returns the upper bound of active eSIM profile count.
1211      *
1212      * <p>The value is an upper bound as eSIMs without profiles are also counted in.
1213      */
1214     protected int getActiveEsimCountUpperBound() throws Exception {
1215         List<Map<String, String>> slots = getTelephonyDumpEntries("UiccSlot");
1216         long count = slots.stream().filter(slot ->
1217                 "true".equals(slot.get("mActive"))
1218                 && "CARDSTATE_PRESENT".equals(slot.get("mCardState"))
1219                 && "true".equals(slot.get("mIsEuicc"))).count();
1220         return Math.toIntExact(count);
1221     }
1222 
1223     protected boolean hasGsmPhone() throws Exception {
1224         // Not using log entries or ServiceState in the dump since they may or may not be present,
1225         // which can make the test flaky
1226         return getTelephonyDumpEntries("Phone").stream()
1227                 .anyMatch(phone ->
1228                         String.format("%d", PHONE_TYPE_GSM).equals(phone.get("getPhoneType()")));
1229     }
1230 
1231     protected boolean hasCdmaPhone() throws Exception {
1232         // Not using log entries or ServiceState in the dump due to the same reason as hasGsmPhone()
1233         return getTelephonyDumpEntries("Phone").stream()
1234                 .anyMatch(phone ->
1235                         String.format("%d", PHONE_TYPE_CDMA).equals(phone.get("getPhoneType()"))
1236                         || String.format("%d", PHONE_TYPE_CDMA_LTE)
1237                                 .equals(phone.get("getPhoneType()")));
1238     }
1239 
1240     // Checks that a timestamp has been truncated to be a multiple of 5 min
1241     protected void assertTimestampIsTruncated(long timestampNs) {
1242         long fiveMinutesInNs = NS_PER_SEC * 5 * 60;
1243         assertWithMessage("Timestamp is not truncated")
1244                 .that(timestampNs % fiveMinutesInNs).isEqualTo(0);
1245     }
1246 
1247     protected List<EventMetricData> backfillAggregatedAtomsInEventMetric(
1248             EventMetricData metricData) {
1249       if (!metricData.hasAggregatedAtomInfo()) {
1250         return Collections.singletonList(metricData);
1251       }
1252       List<EventMetricData> data = new ArrayList<>();
1253       StatsLog.AggregatedAtomInfo atomInfo = metricData.getAggregatedAtomInfo();
1254       for (long timestamp : atomInfo.getElapsedTimestampNanosList()) {
1255         data.add(EventMetricData.newBuilder()
1256                      .setAtom(atomInfo.getAtom())
1257                      .setElapsedTimestampNanos(timestamp)
1258                      .build());
1259       }
1260       return data;
1261     }
1262 }
1263