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