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