Home | History | Annotate | Download | only in atom
      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 android.os.BatteryStatsProto;
     19 import android.service.batterystats.BatteryStatsServiceDumpProto;
     20 import android.view.DisplayStateEnum;
     21 
     22 import com.android.annotations.Nullable;
     23 import com.android.internal.os.StatsdConfigProto.AtomMatcher;
     24 import com.android.internal.os.StatsdConfigProto.EventMetric;
     25 import com.android.internal.os.StatsdConfigProto.FieldFilter;
     26 import com.android.internal.os.StatsdConfigProto.FieldMatcher;
     27 import com.android.internal.os.StatsdConfigProto.FieldValueMatcher;
     28 import com.android.internal.os.StatsdConfigProto.GaugeMetric;
     29 import com.android.internal.os.StatsdConfigProto.Predicate;
     30 import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
     31 import com.android.internal.os.StatsdConfigProto.SimplePredicate;
     32 import com.android.internal.os.StatsdConfigProto.StatsdConfig;
     33 import com.android.internal.os.StatsdConfigProto.TimeUnit;
     34 import com.android.os.AtomsProto.AppBreadcrumbReported;
     35 import com.android.os.AtomsProto.Atom;
     36 import com.android.os.AtomsProto.ScreenStateChanged;
     37 import com.android.os.StatsLog.ConfigMetricsReport;
     38 import com.android.os.StatsLog.ConfigMetricsReportList;
     39 import com.android.os.StatsLog.EventMetricData;
     40 import com.android.os.StatsLog.GaugeMetricData;
     41 import com.android.os.StatsLog.StatsLogReport;
     42 import com.android.tradefed.device.DeviceNotAvailableException;
     43 import com.android.tradefed.log.LogUtil;
     44 
     45 import com.google.common.io.Files;
     46 
     47 import java.io.File;
     48 import java.text.SimpleDateFormat;
     49 import java.util.ArrayList;
     50 import java.util.Arrays;
     51 import java.util.Comparator;
     52 import java.util.Date;
     53 import java.util.List;
     54 import java.util.Set;
     55 import java.util.function.Function;
     56 
     57 import perfetto.protos.PerfettoConfig.DataSourceConfig;
     58 import perfetto.protos.PerfettoConfig.TraceConfig;
     59 import perfetto.protos.PerfettoConfig.TraceConfig.BufferConfig;
     60 import perfetto.protos.PerfettoConfig.TraceConfig.DataSource;
     61 
     62 /**
     63  * Base class for testing Statsd atoms.
     64  * Validates reporting of statsd logging based on different events
     65  */
     66 public class AtomTestCase extends BaseTestCase {
     67 
     68     public static final String UPDATE_CONFIG_CMD = "cmd stats config update";
     69     public static final String DUMP_REPORT_CMD = "cmd stats dump-report";
     70     public static final String DUMP_BATTERYSTATS_CMD = "dumpsys batterystats";
     71     public static final String REMOVE_CONFIG_CMD = "cmd stats config remove";
     72     public static final String CONFIG_UID = "1000";
     73     /** ID of the config, which evaluates to -1572883457. */
     74     public static final long CONFIG_ID = "cts_config".hashCode();
     75 
     76     protected static final int WAIT_TIME_SHORT = 500;
     77     protected static final int WAIT_TIME_LONG = 2_000;
     78 
     79     protected static final long SCREEN_STATE_CHANGE_TIMEOUT = 4000;
     80     protected static final long SCREEN_STATE_POLLING_INTERVAL = 500;
     81 
     82     @Override
     83     protected void setUp() throws Exception {
     84         super.setUp();
     85 
     86         if (statsdDisabled()) {
     87             return;
     88         }
     89         // TODO: need to do these before running real test:
     90         // 1. compile statsd and push to device
     91         // 2. make sure StatsCompanionService and incidentd is running
     92         // 3. start statsd
     93         // These should go away once we have statsd properly set up.
     94 
     95         // Uninstall to clear the history in case it's still on the device.
     96         removeConfig(CONFIG_ID);
     97         getReportList(); // Clears data.
     98     }
     99 
    100     @Override
    101     protected void tearDown() throws Exception {
    102         removeConfig(CONFIG_ID);
    103         super.tearDown();
    104     }
    105 
    106     /**
    107      * Determines whether logcat indicates that incidentd fired since the given device date.
    108      */
    109     protected boolean didIncidentdFireSince(String date) throws Exception {
    110         final String INCIDENTD_TAG = "incidentd";
    111         final String INCIDENTD_STARTED_STRING = "reportIncident";
    112         // TODO: Do something more robust than this in case of delayed logging.
    113         Thread.sleep(1000);
    114         String log = getLogcatSince(date, String.format(
    115                 "-s %s -e %s", INCIDENTD_TAG, INCIDENTD_STARTED_STRING));
    116         return log.contains(INCIDENTD_STARTED_STRING);
    117     }
    118 
    119     /**
    120      * Determines whether logcat indicates that perfetto fired since the given device date.
    121      */
    122     protected boolean didPerfettoStartSince(String date) throws Exception {
    123         final String PERFETTO_TAG = "perfetto";
    124         final String PERFETTO_STARTED_STRING = "Enabled tracing";
    125         final String PERFETTO_STARTED_REGEX = ".*" + PERFETTO_STARTED_STRING + ".*";
    126         // TODO: Do something more robust than this in case of delayed logging.
    127         Thread.sleep(1000);
    128         String log = getLogcatSince(date, String.format(
    129                 "-s %s -e %s", PERFETTO_TAG, PERFETTO_STARTED_REGEX));
    130         return log.contains(PERFETTO_STARTED_STRING);
    131     }
    132 
    133     protected static StatsdConfig.Builder createConfigBuilder() {
    134         return StatsdConfig.newBuilder().setId(CONFIG_ID)
    135                 .addAllowedLogSource("AID_SYSTEM")
    136                 .addAllowedLogSource("AID_BLUETOOTH")
    137                 .addAllowedLogSource(DeviceAtomTestCase.DEVICE_SIDE_TEST_PACKAGE);
    138     }
    139 
    140     protected void createAndUploadConfig(int atomTag) throws Exception {
    141         StatsdConfig.Builder conf = createConfigBuilder();
    142         addAtomEvent(conf, atomTag);
    143         uploadConfig(conf);
    144     }
    145 
    146     protected void uploadConfig(StatsdConfig.Builder config) throws Exception {
    147         uploadConfig(config.build());
    148     }
    149 
    150     protected void uploadConfig(StatsdConfig config) throws Exception {
    151         LogUtil.CLog.d("Uploading the following config:\n" + config.toString());
    152         File configFile = File.createTempFile("statsdconfig", ".config");
    153         configFile.deleteOnExit();
    154         Files.write(config.toByteArray(), configFile);
    155         String remotePath = "/data/local/tmp/" + configFile.getName();
    156         getDevice().pushFile(configFile, remotePath);
    157         getDevice().executeShellCommand(
    158                 String.join(" ", "cat", remotePath, "|", UPDATE_CONFIG_CMD,
    159                         String.valueOf(CONFIG_ID)));
    160         getDevice().executeShellCommand("rm " + remotePath);
    161     }
    162 
    163     protected void removeConfig(long configId) throws Exception {
    164         getDevice().executeShellCommand(
    165                 String.join(" ", REMOVE_CONFIG_CMD, String.valueOf(configId)));
    166     }
    167 
    168     /** Gets the statsd report and sorts it. Note that this also deletes that report from statsd. */
    169     protected List<EventMetricData> getEventMetricDataList() throws Exception {
    170         ConfigMetricsReportList reportList = getReportList();
    171         assertTrue("Expected one report", reportList.getReportsCount() == 1);
    172         ConfigMetricsReport report = reportList.getReports(0);
    173 
    174         List<EventMetricData> data = new ArrayList<>();
    175         for (StatsLogReport metric : report.getMetricsList()) {
    176             data.addAll(metric.getEventMetrics().getDataList());
    177         }
    178         data.sort(Comparator.comparing(EventMetricData::getElapsedTimestampNanos));
    179 
    180         LogUtil.CLog.d("Get EventMetricDataList as following:\n");
    181         for (EventMetricData d : data) {
    182             LogUtil.CLog.d("Atom at " + d.getElapsedTimestampNanos() + ":\n" + d.getAtom().toString());
    183         }
    184         return data;
    185     }
    186 
    187     protected List<Atom> getGaugeMetricDataList() throws Exception {
    188         ConfigMetricsReportList reportList = getReportList();
    189         assertTrue(reportList.getReportsCount() == 1);
    190         // only config
    191         ConfigMetricsReport report = reportList.getReports(0);
    192 
    193         List<Atom> data = new ArrayList<>();
    194         for (GaugeMetricData gaugeMetricData :
    195                 report.getMetrics(0).getGaugeMetrics().getDataList()) {
    196             for (Atom atom : gaugeMetricData.getBucketInfo(0).getAtomList()) {
    197                 data.add(atom);
    198             }
    199         }
    200 
    201         LogUtil.CLog.d("Get GaugeMetricDataList as following:\n");
    202         for (Atom d : data) {
    203             LogUtil.CLog.d("Atom:\n" + d.toString());
    204         }
    205         return data;
    206     }
    207 
    208     protected StatsLogReport getStatsLogReport() throws Exception {
    209         ConfigMetricsReportList reportList = getReportList();
    210         assertTrue(reportList.getReportsCount() == 1);
    211         ConfigMetricsReport report = reportList.getReports(0);
    212         assertTrue(report.hasUidMap());
    213         assertEquals(1, report.getMetricsCount());
    214         return report.getMetrics(0);
    215     }
    216 
    217     /** Gets the statsd report. Note that this also deletes that report from statsd. */
    218     protected ConfigMetricsReportList getReportList() throws Exception {
    219         try {
    220             ConfigMetricsReportList reportList = getDump(ConfigMetricsReportList.parser(),
    221                     String.join(" ", DUMP_REPORT_CMD, String.valueOf(CONFIG_ID),
    222                             "--proto"));
    223             return reportList;
    224         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
    225             LogUtil.CLog.e("Failed to fetch and parse the statsd output report. "
    226                     + "Perhaps there is not a valid statsd config for the requested "
    227                     + "uid=" + CONFIG_UID + ", id=" + CONFIG_ID + ".");
    228             throw (e);
    229         }
    230     }
    231 
    232     protected BatteryStatsProto getBatteryStatsProto() throws Exception {
    233         try {
    234             BatteryStatsProto batteryStatsProto = getDump(BatteryStatsServiceDumpProto.parser(),
    235                     String.join(" ", DUMP_BATTERYSTATS_CMD,
    236                             "--proto")).getBatterystats();
    237             LogUtil.CLog.d("Got batterystats:\n " + batteryStatsProto.toString());
    238             return batteryStatsProto;
    239         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
    240             LogUtil.CLog.e("Failed to dump batterystats proto");
    241             throw (e);
    242         }
    243     }
    244 
    245     /** Creates a FieldValueMatcher.Builder corresponding to the given field. */
    246     protected static FieldValueMatcher.Builder createFvm(int field) {
    247         return FieldValueMatcher.newBuilder().setField(field);
    248     }
    249 
    250     protected static TraceConfig createPerfettoTraceConfig() {
    251         return TraceConfig.newBuilder()
    252             .addBuffers(BufferConfig.newBuilder().setSizeKb(32))
    253             .addDataSources(DataSource.newBuilder()
    254                 .setConfig(DataSourceConfig.newBuilder()
    255                     .setName("linux.ftrace")
    256                     .setTargetBuffer(0)
    257                     .build()
    258                 )
    259             )
    260             .build();
    261     }
    262 
    263     protected void addAtomEvent(StatsdConfig.Builder conf, int atomTag) throws Exception {
    264         addAtomEvent(conf, atomTag, new ArrayList<FieldValueMatcher.Builder>());
    265     }
    266 
    267     /**
    268      * Adds an event to the config for an atom that matches the given key.
    269      *
    270      * @param conf    configuration
    271      * @param atomTag atom tag (from atoms.proto)
    272      * @param fvm     FieldValueMatcher.Builder for the relevant key
    273      */
    274     protected void addAtomEvent(StatsdConfig.Builder conf, int atomTag,
    275             FieldValueMatcher.Builder fvm)
    276             throws Exception {
    277         addAtomEvent(conf, atomTag, Arrays.asList(fvm));
    278     }
    279 
    280     /**
    281      * Adds an event to the config for an atom that matches the given keys.
    282      *
    283      * @param conf   configuration
    284      * @param atomId atom tag (from atoms.proto)
    285      * @param fvms   list of FieldValueMatcher.Builders to attach to the atom. May be null.
    286      */
    287     protected void addAtomEvent(StatsdConfig.Builder conf, int atomId,
    288             List<FieldValueMatcher.Builder> fvms) throws Exception {
    289 
    290         final String atomName = "Atom" + System.nanoTime();
    291         final String eventName = "Event" + System.nanoTime();
    292 
    293         SimpleAtomMatcher.Builder sam = SimpleAtomMatcher.newBuilder().setAtomId(atomId);
    294         if (fvms != null) {
    295             for (FieldValueMatcher.Builder fvm : fvms) {
    296                 sam.addFieldValueMatcher(fvm);
    297             }
    298         }
    299         conf.addAtomMatcher(AtomMatcher.newBuilder()
    300                 .setId(atomName.hashCode())
    301                 .setSimpleAtomMatcher(sam));
    302         conf.addEventMetric(EventMetric.newBuilder()
    303                 .setId(eventName.hashCode())
    304                 .setWhat(atomName.hashCode()));
    305     }
    306 
    307     /**
    308      * Adds an atom to a gauge metric of a config
    309      *
    310      * @param conf      configuration
    311      * @param atomId    atom id (from atoms.proto)
    312      * @param dimension dimension is needed for most pulled atoms
    313      */
    314     protected void addGaugeAtom(StatsdConfig.Builder conf, int atomId,
    315             @Nullable FieldMatcher.Builder dimension) throws Exception {
    316         final String atomName = "Atom" + System.nanoTime();
    317         final String gaugeName = "Gauge" + System.nanoTime();
    318         final String predicateName = "SCREEN_IS_ON";
    319         SimpleAtomMatcher.Builder sam = SimpleAtomMatcher.newBuilder().setAtomId(atomId);
    320         conf.addAtomMatcher(AtomMatcher.newBuilder()
    321                 .setId(atomName.hashCode())
    322                 .setSimpleAtomMatcher(sam));
    323         // TODO: change this predicate to something simpler and easier
    324         final String predicateTrueName = "SCREEN_TURNED_ON";
    325         final String predicateFalseName = "SCREEN_TURNED_OFF";
    326         conf.addAtomMatcher(AtomMatcher.newBuilder()
    327                 .setId(predicateTrueName.hashCode())
    328                 .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
    329                         .setAtomId(Atom.SCREEN_STATE_CHANGED_FIELD_NUMBER)
    330                         .addFieldValueMatcher(FieldValueMatcher.newBuilder()
    331                                 .setField(ScreenStateChanged.STATE_FIELD_NUMBER)
    332                                 .setEqInt(DisplayStateEnum.DISPLAY_STATE_ON_VALUE)
    333                         )
    334                 )
    335         )
    336                 // Used to trigger predicate
    337                 .addAtomMatcher(AtomMatcher.newBuilder()
    338                         .setId(predicateFalseName.hashCode())
    339                         .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
    340                                 .setAtomId(Atom.SCREEN_STATE_CHANGED_FIELD_NUMBER)
    341                                 .addFieldValueMatcher(FieldValueMatcher.newBuilder()
    342                                         .setField(ScreenStateChanged.STATE_FIELD_NUMBER)
    343                                         .setEqInt(DisplayStateEnum.DISPLAY_STATE_OFF_VALUE)
    344                                 )
    345                         )
    346                 );
    347         conf.addPredicate(Predicate.newBuilder()
    348                 .setId(predicateName.hashCode())
    349                 .setSimplePredicate(SimplePredicate.newBuilder()
    350                         .setStart(predicateTrueName.hashCode())
    351                         .setStop(predicateFalseName.hashCode())
    352                         .setCountNesting(false)
    353                 )
    354         );
    355         GaugeMetric.Builder gaugeMetric = GaugeMetric.newBuilder()
    356                 .setId(gaugeName.hashCode())
    357                 .setWhat(atomName.hashCode())
    358                 .setGaugeFieldsFilter(FieldFilter.newBuilder().setIncludeAll(true).build())
    359                 .setSamplingType(GaugeMetric.SamplingType.ALL_CONDITION_CHANGES)
    360                 .setBucket(TimeUnit.CTS)
    361                 .setCondition(predicateName.hashCode());
    362         if (dimension != null) {
    363             gaugeMetric.setDimensionsInWhat(dimension.build());
    364         }
    365         conf.addGaugeMetric(gaugeMetric.build());
    366     }
    367 
    368     /**
    369      * Asserts that each set of states in stateSets occurs at least once in data.
    370      * Asserts that the states in data occur in the same order as the sets in stateSets.
    371      *
    372      * @param stateSets        A list of set of states, where each set represents an equivalent
    373      *                         state of the device for the purpose of CTS.
    374      * @param data             list of EventMetricData from statsd, produced by
    375      *                         getReportMetricListData()
    376      * @param wait             expected duration (in ms) between state changes; asserts that the
    377      *                         actual wait
    378      *                         time was wait/2 <= actual_wait <= 5*wait. Use 0 to ignore this
    379      *                         assertion.
    380      * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
    381      */
    382     public void assertStatesOccurred(List<Set<Integer>> stateSets, List<EventMetricData> data,
    383             int wait, Function<Atom, Integer> getStateFromAtom) {
    384         // Sometimes, there are more events than there are states.
    385         // Eg: When the screen turns off, it may go into OFF and then DOZE immediately.
    386         assertTrue("Too few states found (" + data.size() + ")", data.size() >= stateSets.size());
    387         int stateSetIndex = 0; // Tracks which state set we expect the data to be in.
    388         for (int dataIndex = 0; dataIndex < data.size(); dataIndex++) {
    389             Atom atom = data.get(dataIndex).getAtom();
    390             int state = getStateFromAtom.apply(atom);
    391             // If state is in the current state set, we do not assert anything.
    392             // If it is not, we expect to have transitioned to the next state set.
    393             if (stateSets.get(stateSetIndex).contains(state)) {
    394                 // No need to assert anything. Just log it.
    395                 LogUtil.CLog.i("The following atom at dataIndex=" + dataIndex + " is "
    396                         + "in stateSetIndex " + stateSetIndex + ":\n"
    397                         + data.get(dataIndex).getAtom().toString());
    398             } else {
    399                 stateSetIndex += 1;
    400                 LogUtil.CLog.i("Assert that the following atom at dataIndex=" + dataIndex + " is"
    401                         + " in stateSetIndex " + stateSetIndex + ":\n"
    402                         + data.get(dataIndex).getAtom().toString());
    403                 assertTrue("Missed first state", dataIndex != 0); // should not be on first data
    404                 assertTrue("Too many states (" + (stateSetIndex + 1) + ")",
    405                         stateSetIndex < stateSets.size());
    406                 assertTrue("Is in wrong state (" + state + ")",
    407                         stateSets.get(stateSetIndex).contains(state));
    408                 if (wait > 0) {
    409                     assertTimeDiffBetween(data.get(dataIndex - 1), data.get(dataIndex),
    410                             wait / 2, wait * 5);
    411                 }
    412             }
    413         }
    414         assertTrue("Too few states (" + (stateSetIndex + 1) + ")",
    415                 stateSetIndex == stateSets.size() - 1);
    416     }
    417 
    418     /**
    419      * Removes all elements from data prior to the first occurrence of an element of state. After
    420      * this method is called, the first element of data (if non-empty) is guaranteed to be an
    421      * element in state.
    422      *
    423      * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
    424      */
    425     public void popUntilFind(List<EventMetricData> data, Set<Integer> state,
    426             Function<Atom, Integer> getStateFromAtom) {
    427         int firstStateIdx;
    428         for (firstStateIdx = 0; firstStateIdx < data.size(); firstStateIdx++) {
    429             Atom atom = data.get(firstStateIdx).getAtom();
    430             if (state.contains(getStateFromAtom.apply(atom))) {
    431                 break;
    432             }
    433         }
    434         if (firstStateIdx == 0) {
    435             // First first element already is in state, so there's nothing to do.
    436             return;
    437         }
    438         data.subList(0, firstStateIdx).clear();
    439     }
    440 
    441     /**
    442      * Removes all elements from data after to the last occurrence of an element of state. After
    443      * this method is called, the last element of data (if non-empty) is guaranteed to be an
    444      * element in state.
    445      *
    446      * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
    447      */
    448     public void popUntilFindFromEnd(List<EventMetricData> data, Set<Integer> state,
    449         Function<Atom, Integer> getStateFromAtom) {
    450         int lastStateIdx;
    451         for (lastStateIdx = data.size() - 1; lastStateIdx >= 0; lastStateIdx--) {
    452             Atom atom = data.get(lastStateIdx).getAtom();
    453             if (state.contains(getStateFromAtom.apply(atom))) {
    454                 break;
    455             }
    456         }
    457         if (lastStateIdx == data.size()-1) {
    458             // Last element already is in state, so there's nothing to do.
    459             return;
    460         }
    461         data.subList(lastStateIdx+1, data.size()).clear();
    462     }
    463 
    464     /** Returns the UID of the host, which should always either be SHELL (2000) or ROOT (0). */
    465     protected int getHostUid() throws DeviceNotAvailableException {
    466         String strUid = "";
    467         try {
    468             strUid = getDevice().executeShellCommand("id -u");
    469             return Integer.parseInt(strUid.trim());
    470         } catch (NumberFormatException e) {
    471             LogUtil.CLog.e("Failed to get host's uid via shell command. Found " + strUid);
    472             // Fall back to alternative method...
    473             if (getDevice().isAdbRoot()) {
    474                 return 0; // ROOT
    475             } else {
    476                 return 2000; // SHELL
    477             }
    478         }
    479     }
    480 
    481     protected void turnScreenOn() throws Exception {
    482         getDevice().executeShellCommand("input keyevent KEYCODE_WAKEUP");
    483         getDevice().executeShellCommand("wm dismiss-keyguard");
    484     }
    485 
    486     protected void turnScreenOff() throws Exception {
    487         getDevice().executeShellCommand("input keyevent KEYCODE_SLEEP");
    488     }
    489 
    490     protected void setChargingState(int state) throws Exception {
    491         getDevice().executeShellCommand("cmd battery set status " + state);
    492     }
    493 
    494     protected void unplugDevice() throws Exception {
    495         getDevice().executeShellCommand("cmd battery unplug");
    496     }
    497 
    498     protected void plugInAc() throws Exception {
    499         getDevice().executeShellCommand("cmd battery set ac 1");
    500     }
    501 
    502     protected void plugInUsb() throws Exception {
    503         getDevice().executeShellCommand("cmd battery set usb 1");
    504     }
    505 
    506     protected void plugInWireless() throws Exception {
    507         getDevice().executeShellCommand("cmd battery set wireless 1");
    508     }
    509 
    510     public void doAppBreadcrumbReportedStart(int label) throws Exception {
    511         doAppBreadcrumbReported(label, AppBreadcrumbReported.State.START.ordinal());
    512     }
    513 
    514     public void doAppBreadcrumbReportedStop(int label) throws Exception {
    515         doAppBreadcrumbReported(label, AppBreadcrumbReported.State.STOP.ordinal());
    516     }
    517 
    518     public void doAppBreadcrumbReported(int label, int state) throws Exception {
    519         getDevice().executeShellCommand(String.format(
    520                 "cmd stats log-app-breadcrumb %d %d", label, state));
    521     }
    522 
    523     protected void setBatteryLevel(int level) throws Exception {
    524         getDevice().executeShellCommand("cmd battery set level " + level);
    525     }
    526 
    527     protected void resetBatteryStatus() throws Exception {
    528         getDevice().executeShellCommand("cmd battery reset");
    529     }
    530 
    531     protected int getScreenBrightness() throws Exception {
    532         return Integer.parseInt(
    533                 getDevice().executeShellCommand("settings get system screen_brightness").trim());
    534     }
    535 
    536     protected void setScreenBrightness(int brightness) throws Exception {
    537         getDevice().executeShellCommand("settings put system screen_brightness " + brightness);
    538     }
    539 
    540     protected boolean isScreenBrightnessModeManual() throws Exception {
    541         String mode = getDevice().executeShellCommand("settings get system screen_brightness_mode");
    542         return Integer.parseInt(mode.trim()) == 0;
    543     }
    544 
    545     protected void setScreenBrightnessMode(boolean manual) throws Exception {
    546         getDevice().executeShellCommand(
    547                 "settings put system screen_brightness_mode " + (manual ? 0 : 1));
    548     }
    549 
    550     protected void enterDozeModeLight() throws Exception {
    551         getDevice().executeShellCommand("dumpsys deviceidle force-idle light");
    552     }
    553 
    554     protected void enterDozeModeDeep() throws Exception {
    555         getDevice().executeShellCommand("dumpsys deviceidle force-idle deep");
    556     }
    557 
    558     protected void leaveDozeMode() throws Exception {
    559         getDevice().executeShellCommand("dumpsys deviceidle unforce");
    560         getDevice().executeShellCommand("dumpsys deviceidle disable");
    561         getDevice().executeShellCommand("dumpsys deviceidle enable");
    562     }
    563 
    564     protected void turnBatterySaverOn() throws Exception {
    565         getDevice().executeShellCommand("cmd battery unplug");
    566         getDevice().executeShellCommand("settings put global low_power 1");
    567     }
    568 
    569     protected void turnBatterySaverOff() throws Exception {
    570         getDevice().executeShellCommand("settings put global low_power 0");
    571         getDevice().executeShellCommand("cmd battery reset");
    572     }
    573 
    574     protected void rebootDevice() throws Exception {
    575         getDevice().rebootUntilOnline();
    576     }
    577 
    578     /**
    579      * Asserts that the two events are within the specified range of each other.
    580      *
    581      * @param d0        the event that should occur first
    582      * @param d1        the event that should occur second
    583      * @param minDiffMs d0 should precede d1 by at least this amount
    584      * @param maxDiffMs d0 should precede d1 by at most this amount
    585      */
    586     public static void assertTimeDiffBetween(EventMetricData d0, EventMetricData d1,
    587             int minDiffMs, int maxDiffMs) {
    588         long diffMs = (d1.getElapsedTimestampNanos() - d0.getElapsedTimestampNanos()) / 1_000_000;
    589         assertTrue("Illegal time difference (" + diffMs + "ms)", minDiffMs <= diffMs);
    590         assertTrue("Illegal time difference (" + diffMs + "ms)", diffMs <= maxDiffMs);
    591     }
    592 
    593     protected String getCurrentLogcatDate() throws Exception {
    594         // TODO: Do something more robust than this for getting logcat markers.
    595         long timestampMs = getDevice().getDeviceDate();
    596         return new SimpleDateFormat("MM-dd HH:mm:ss.SSS")
    597                 .format(new Date(timestampMs));
    598     }
    599 
    600     protected String getLogcatSince(String date, String logcatParams) throws Exception {
    601         return getDevice().executeShellCommand(String.format(
    602                 "logcat -v threadtime -t '%s' -d %s", date, logcatParams));
    603     }
    604 
    605     /**
    606      * Pulled atoms should have a better way of constructing the config.
    607      * Remove this config when that happens.
    608      */
    609     protected StatsdConfig.Builder getPulledConfig() {
    610         return StatsdConfig.newBuilder().setId(CONFIG_ID)
    611                 .addAllowedLogSource("AID_SYSTEM")
    612                 .addAllowedLogSource(DeviceAtomTestCase.DEVICE_SIDE_TEST_PACKAGE);
    613     }
    614 
    615     /**
    616      * Determines if the device has the given feature.
    617      * Prints a warning if its value differs from requiredAnswer.
    618      */
    619     protected boolean hasFeature(String featureName, boolean requiredAnswer) throws Exception {
    620         final String features = getDevice().executeShellCommand("pm list features");
    621         boolean hasIt = features.contains(featureName);
    622         if (hasIt != requiredAnswer) {
    623             LogUtil.CLog.w("Device does " + (requiredAnswer ? "not " : "") + "have feature "
    624                     + featureName);
    625         }
    626         return hasIt == requiredAnswer;
    627     }
    628 
    629 }
    630