Home | History | Annotate | Download | only in collectors
      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.device.collectors;
     17 
     18 import android.app.UiAutomation;
     19 import android.device.collectors.annotations.OptionClass;
     20 import android.os.Build;
     21 import android.os.Bundle;
     22 import android.os.ParcelFileDescriptor;
     23 import android.support.annotation.VisibleForTesting;
     24 import android.util.Log;
     25 
     26 import org.junit.runner.Description;
     27 import org.junit.runner.Result;
     28 
     29 import java.io.BufferedOutputStream;
     30 import java.io.BufferedReader;
     31 import java.io.File;
     32 import java.io.FileOutputStream;
     33 import java.io.IOException;
     34 import java.io.InputStream;
     35 import java.io.InputStreamReader;
     36 import java.io.OutputStream;
     37 
     38 /**
     39  * A {@link BaseMetricListener} that captures BatteryStats for the entire test class in proto format
     40  * and record it to a file.
     41  *
     42  * This class needs external storage permission. See {@link BaseMetricListener} how to grant
     43  * external storage permission, especially at install time.
     44  *
     45  * Options:
     46  * -e batterystats-format [byte|file:optional/path/to/dir] : set storage format. Default is file.
     47  * Choose "byte" to save as byte array in result bundle.
     48  * Choose "file" to save as proto files. Append ":path/to/dir" behind "file" to specify directory
     49  * to save the files, relative to /sdcard/. e.g. "-e batterystats-format file:tmp/bs" will save
     50  * batterystats protobuf to /sdcard/tmp/bs/ directory.
     51  *
     52  * Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
     53  * collection fails.
     54  */
     55 @OptionClass(alias = "battery-stats-collector")
     56 public class BatteryStatsListener extends BaseMetricListener {
     57     private static final String MSG_DUMPSYS_RESET_SUCCESS = "Battery stats reset.";
     58     public  static final String CMD_DUMPSYS = "dumpsys batterystats --proto";
     59     private static final String CMD_DUMPSYS_RESET = "dumpsys batterystats --reset";
     60     public static final String OPTION_BYTE = "byte";
     61     static final String DEFAULT_DIR = "run_listeners/battery_stats";
     62     static final String KEY_PER_RUN = "batterystats-per-run";
     63     static final String KEY_FORMAT = "batterystats-format";
     64 
     65     private File mDestDir;
     66     private String mTestClassName;
     67     private boolean mPerRun;
     68     private boolean mBatteryStatReset;
     69     private boolean mToFile;
     70 
     71     public BatteryStatsListener() {
     72         super();
     73     }
     74 
     75     /**
     76      * Constructor to simulate receiving the instrumentation arguments. Should not be used except
     77      * for testing.
     78      */
     79     @VisibleForTesting
     80     BatteryStatsListener(Bundle argsBundle) {
     81         super(argsBundle);
     82     }
     83 
     84     @Override
     85     public void onTestRunStart(DataRecord runData, Description description) {
     86         if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
     87             Log.w(getTag(), "dumpsys batterystats requires API version >= 21");
     88             return;
     89         }
     90         Bundle args = getArgsBundle();
     91         mPerRun = "true".equals(args.getString(KEY_PER_RUN));
     92         mToFile = !OPTION_BYTE.equals(args.getString(KEY_FORMAT));
     93         if (mToFile) {
     94             String dir = DEFAULT_DIR;
     95             if (args.containsKey(KEY_FORMAT)) {
     96                 String[] argsArray = args.getString(KEY_FORMAT).split(":", 2);
     97                 if (argsArray.length > 1) dir = argsArray[1];
     98             }
     99             mDestDir = createAndEmptyDirectory(dir);
    100         }
    101 
    102         // set charging state as unplugged
    103         executeCommandBlocking("dumpsys battery unplug");
    104         if (mPerRun) {
    105             mBatteryStatReset = resetBatteryStats();
    106         }
    107     }
    108 
    109     @Override
    110     public void onTestStart(DataRecord testData, Description description) {
    111         if (mToFile && mDestDir == null) {
    112             return;
    113         }
    114         // a workaround to get test class name. description provided to onTestRunStart is null.
    115         if (mTestClassName == null) {
    116             mTestClassName = description.getClassName();
    117         }
    118         if (mPerRun) {
    119             return;
    120         }
    121         mBatteryStatReset = resetBatteryStats();
    122     }
    123 
    124     @Override
    125     public void onTestEnd(DataRecord testData, Description description) {
    126         if ((mToFile && mDestDir == null) || mPerRun || !mBatteryStatReset) {
    127             return;
    128         }
    129         mBatteryStatReset = false;
    130         if (mToFile) {
    131             String fileName = String.format("%s.%s.batterystatsproto", description.getClassName(),
    132                     description.getMethodName());
    133             File logFile = dumpBatteryStats(fileName);
    134             if (logFile != null) {
    135                 testData.addFileMetric(String.format("%s_%s", getTag(), logFile.getName()), logFile);
    136             }
    137         } else {
    138             String key = String.format("%s_%s.%s.bytes", getTag(), description.getClassName(),
    139                     description.getMethodName());
    140             byte[] proto = executeCommandBlocking(CMD_DUMPSYS);
    141             if (proto != null) {
    142                 testData.addBinaryMetric(key, proto);
    143             }
    144         }
    145     }
    146 
    147     @Override
    148     public void onTestRunEnd(DataRecord runData, Result result) {
    149         if (mToFile && mDestDir == null) {
    150             return;
    151         }
    152 
    153         if (mPerRun && mBatteryStatReset) {
    154             mBatteryStatReset = false;
    155             if (mToFile) {
    156                 File logFile = dumpBatteryStats(String.format("%s.batterystatsproto", mTestClassName));
    157                 if (logFile != null) {
    158                     runData.addFileMetric(String.format("%s_%s", getTag(), logFile.getName()), logFile);
    159                 }
    160             } else {
    161                 String key = String.format("%s_%s.bytes", getTag(), mTestClassName);
    162                 byte[] proto = executeCommandBlocking(CMD_DUMPSYS);
    163                 if (proto != null) {
    164                     runData.addBinaryMetric(key, proto);
    165                 }
    166             }
    167         }
    168         // reset charging state
    169         executeCommandBlocking("dumpsys battery reset");
    170     }
    171 
    172     /**
    173      * Call "dumpsys batterystats --proto" to dump batterystats to a proto file.
    174      * Public so that Mockito can alter its behavior.
    175      *
    176      * @param fileName the name of the proto file
    177      * @return the proto file containing the dumpsys batterystats
    178      */
    179     @VisibleForTesting
    180     public File dumpBatteryStats(String fileName) {
    181         UiAutomation automation = getInstrumentation().getUiAutomation();
    182         File logFile = new File(mDestDir, fileName);
    183         try (
    184                 InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(
    185                         automation.executeShellCommand(CMD_DUMPSYS));
    186                 OutputStream out = new BufferedOutputStream(new FileOutputStream(logFile))
    187         ){
    188             byte[] buf = new byte[BUFFER_SIZE];
    189             int bytes;
    190             while((bytes = is.read(buf)) != -1) {
    191                 out.write(buf, 0, bytes);
    192             }
    193             return logFile;
    194         } catch (Exception e) {
    195             Log.e(getTag(), "Unable to dump batterystats", e);
    196             return null;
    197         }
    198     }
    199 
    200     /**
    201      * Call "dumpsys batterystats --reset" to reset batterystats.
    202      * Public so that Mockito can alter its behavior.
    203      *
    204      * @return true only if reset successfully without error
    205      */
    206     @VisibleForTesting
    207     public boolean resetBatteryStats() {
    208         UiAutomation automation = getInstrumentation().getUiAutomation();
    209         try (
    210                 InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(
    211                         automation.executeShellCommand(CMD_DUMPSYS_RESET));
    212                 BufferedReader reader = new BufferedReader(new InputStreamReader(is))
    213         ){
    214             String line;
    215             while(null != (line = reader.readLine())) {
    216                 if(line.contains(MSG_DUMPSYS_RESET_SUCCESS)) {
    217                     return true;
    218                 }
    219             }
    220             Log.e(getTag(), "Unable to reset batterystats");
    221             return false;
    222         } catch (IOException ex) {
    223             Log.e(getTag(), "Unable to reset batterystats", ex);
    224             return false;
    225         }
    226     }
    227 }
    228