1 /*
2  * Copyright (C) 2011 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 
17 package com.android.tradefed.testtype;
18 
19 import com.android.annotations.VisibleForTesting;
20 import com.android.tradefed.config.Option;
21 import com.android.tradefed.config.OptionClass;
22 import com.android.tradefed.device.DeviceNotAvailableException;
23 import com.android.tradefed.device.DeviceSelectionOptions;
24 import com.android.tradefed.device.ITestDevice;
25 import com.android.tradefed.invoker.TestInformation;
26 import com.android.tradefed.log.LogUtil.CLog;
27 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
28 import com.android.tradefed.result.FailureDescription;
29 import com.android.tradefed.result.ITestInvocationListener;
30 import com.android.tradefed.result.ITestLifeCycleReceiver;
31 import com.android.tradefed.result.TestDescription;
32 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
33 import com.android.tradefed.util.IRunUtil;
34 import com.android.tradefed.util.RunUtil;
35 import com.android.tradefed.util.TimeUtil;
36 
37 import org.junit.Assert;
38 
39 import java.util.HashMap;
40 
41 /**
42  * An {@link IRemoteTest} that checks for a minimum battery charge, and waits for the battery to
43  * reach a second charging threshold if the minimum charge isn't present.
44  */
45 @OptionClass(alias = "battery-checker")
46 public class DeviceBatteryLevelChecker implements IRemoteTest {
47     private static final Integer IGNORE_CHARGE = -101;
48 
49     private ITestDevice mTestDevice = null;
50     private TestDescription mTestDescription = new TestDescription("BatteryCharging", "charge");
51     private TestDescription mChargingSpeed = new TestDescription("BatteryCharging", "speed");
52 
53     /**
54      * We use max-battery here to coincide with a {@link DeviceSelectionOptions} option of the same
55      * name.  Thus, DeviceBatteryLevelChecker
56      */
57     @Option(name = "max-battery", description = "Charge level below which we force the device to " +
58             "sit and charge.  Range: 0-100.")
59     private Integer mMaxBattery = 20;
60 
61     @Option(name = "resume-level", description = "Charge level at which we release the device to " +
62             "begin testing again. Range: 0-100.")
63     private int mResumeLevel = 80;
64 
65     /**
66      * This is decoupled from the log poll time below specifically to allow this invocation to be
67      * killed without having to wait for the full log period to lapse.
68      */
69     @Option(name = "poll-time", description = "Time in minutes to wait between battery level " +
70             "polls. Decimal times accepted.")
71     private double mChargingPollTime = 1.0;
72 
73     @Option(name = "batt-log-period", description = "Min time in minutes to wait between " +
74             "printing current battery level to log.  Decimal times accepted.")
75     private double mLoggingPollTime = 10.0;
76 
77     @Option(name = "reboot-charging-devices", description = "Whether to reboot a device when we " +
78             "detect that it should be held for charging.  This would hopefully kill any battery-" +
79             "draining processes and allow the device to charge at its fastest rate.")
80     private boolean mRebootChargeDevices = false;
81 
82     @Option(name = "stop-runtime", description = "Whether to stop runtime.")
83     private boolean mStopRuntime = false;
84 
85     @Option(name = "stop-logcat", description = "Whether to stop logcat during the recharge. "
86             + "this option is enabled by default.")
87     private boolean mStopLogcat = true;
88 
89     @Option(
90         name = "max-run-time",
91         description = "The max run time the battery level checker can run before stopping.",
92         isTimeVal = true
93     )
94     private long mMaxRunTime = 30 * 60 * 1000L;
95 
96     @Option(
97             name = "reference-charging-speed",
98             description = "The expected charging speed in % per hours.")
99     private Integer mChargingSpeedCheck = 15;
100 
checkBatteryLevel(ITestDevice device)101     Integer checkBatteryLevel(ITestDevice device) {
102         return device.getBattery();
103     }
104 
stopDeviceRuntime()105     private void stopDeviceRuntime() throws DeviceNotAvailableException {
106         mTestDevice.executeShellCommand("stop");
107     }
108 
startDeviceRuntime()109     private void startDeviceRuntime() throws DeviceNotAvailableException {
110         mTestDevice.executeShellCommand("start");
111         mTestDevice.waitForDeviceAvailable();
112     }
113 
114     /** {@inheritDoc} */
115     @Override
run(TestInformation testInfo, ITestInvocationListener listener)116     public void run(TestInformation testInfo, ITestInvocationListener listener)
117             throws DeviceNotAvailableException {
118         mTestDevice = testInfo.getDevice();
119         Assert.assertNotNull(mTestDevice);
120 
121         long startTime = System.currentTimeMillis();
122         int testCount = 1;
123         boolean chargeCheck = false;
124         if (mChargingSpeedCheck != null && mChargingSpeedCheck > 0) {
125             testCount++;
126             chargeCheck = true;
127         }
128         listener.testRunStarted("BatteryCharging", testCount);
129         listener.testStarted(mTestDescription);
130         try {
131             Integer charge = null;
132             long elapsedTimeMs = getCurrentTimeMs();
133             try {
134                 charge = runTest(testInfo, listener);
135                 elapsedTimeMs = getCurrentTimeMs() - elapsedTimeMs;
136             } catch (DeviceNotAvailableException e) {
137                 FailureDescription failure =
138                         FailureDescription.create(e.getMessage())
139                                 .setCause(e)
140                                 .setErrorIdentifier(e.getErrorId())
141                                 .setOrigin(e.getOrigin());
142                 if (e.getErrorId() != null) {
143                     failure.setFailureStatus(e.getErrorId().status());
144                 }
145                 listener.testRunFailed(failure);
146                 throw e;
147             } finally {
148                 listener.testEnded(mTestDescription, new HashMap<String, Metric>());
149             }
150             if (chargeCheck) {
151                 listener.testStarted(mChargingSpeed);
152                 if (charge == null) {
153                     FailureDescription failure =
154                             FailureDescription.create("No battery charge information");
155                     failure.setFailureStatus(FailureStatus.NOT_EXECUTED);
156                     listener.testFailed(mChargingSpeed, failure);
157                 } else if (IGNORE_CHARGE.equals(charge)) {
158                     listener.testIgnored(mChargingSpeed);
159                 } else {
160                     checkChargingSpeed(listener, charge, elapsedTimeMs);
161                 }
162                 listener.testEnded(mChargingSpeed, new HashMap<String, Metric>());
163             }
164         } finally {
165             listener.testRunEnded(
166                     System.currentTimeMillis() - startTime, new HashMap<String, Metric>());
167         }
168     }
169 
runTest(TestInformation testInfo, ITestInvocationListener listener)170     private Integer runTest(TestInformation testInfo, ITestInvocationListener listener)
171             throws DeviceNotAvailableException {
172         mTestDevice = testInfo.getDevice();
173         Assert.assertNotNull(mTestDevice);
174 
175         Integer batteryLevel = checkBatteryLevel(mTestDevice);
176 
177         if (batteryLevel == null) {
178             CLog.w("Failed to determine battery level for device %s.",
179                     mTestDevice.getSerialNumber());
180             listener.testFailed(
181                     mTestDescription,
182                     FailureDescription.create("Failed to determine battery level"));
183             return null;
184         } else if (batteryLevel < mMaxBattery) {
185             // Time-out.  Send the device to the corner
186             CLog.w("Battery level %d is below the min level %d; holding for device %s to charge " +
187                     "to level %d", batteryLevel, mMaxBattery, mTestDevice.getSerialNumber(),
188                     mResumeLevel);
189         } else {
190             // Good to go
191             CLog.d("Battery level %d is above the minimum of %d; %s is good to go.", batteryLevel,
192                     mMaxBattery, mTestDevice.getSerialNumber());
193             return IGNORE_CHARGE;
194         }
195 
196         if (mRebootChargeDevices) {
197             // reboot the device, in an attempt to kill any battery-draining processes
198             CLog.d("Rebooting device %s prior to holding", mTestDevice.getSerialNumber());
199             mTestDevice.reboot();
200         }
201 
202         // turn screen off
203         turnScreenOff(mTestDevice);
204 
205         Integer finalBattery = null;
206         try {
207             if (mStopRuntime) {
208                 stopDeviceRuntime();
209             }
210             // Stop our logcat receiver
211             if (mStopLogcat) {
212                 mTestDevice.stopLogcat();
213             }
214 
215             finalBattery = runBatteryCharging(listener, mTestDescription);
216         } finally {
217             if (mStopRuntime) {
218                 // Restart runtime if it was stopped
219                 startDeviceRuntime();
220             }
221         }
222         CLog.i(
223                 "Device %s is now charged to battery level %d; releasing.",
224                 mTestDevice.getSerialNumber(), batteryLevel);
225         if (finalBattery != null) {
226             return finalBattery - batteryLevel;
227         }
228         return null;
229     }
230 
turnScreenOff(ITestDevice device)231     private void turnScreenOff(ITestDevice device) throws DeviceNotAvailableException {
232         // TODO: Handle the case where framework is not working, both command below require it.
233         // disable always on
234         device.executeShellCommand("svc power stayon false");
235         // set screen timeout to 1s
236         device.executeShellCommand("settings put system screen_off_timeout 1000");
237         // pause for 5s to ensure that screen would be off
238         getRunUtil().sleep(5000);
239     }
240 
runBatteryCharging(ITestLifeCycleReceiver listener, TestDescription test)241     private Integer runBatteryCharging(ITestLifeCycleReceiver listener, TestDescription test) {
242         // If we're down here, it's time to hold the device until it reaches mResumeLevel
243         Long lastReportTime = System.currentTimeMillis();
244         Integer batteryLevel = checkBatteryLevel(mTestDevice);
245 
246         long startTime = System.currentTimeMillis();
247         while (batteryLevel != null && batteryLevel < mResumeLevel) {
248             if (System.currentTimeMillis() - lastReportTime > mLoggingPollTime * 60 * 1000) {
249                 // Log the battery level status every mLoggingPollTime minutes
250                 CLog.i(
251                         "Battery level for device %s is currently %d",
252                         mTestDevice.getSerialNumber(), batteryLevel);
253                 lastReportTime = System.currentTimeMillis();
254             }
255             if (System.currentTimeMillis() - startTime > mMaxRunTime) {
256                 CLog.i(
257                         "DeviceBatteryLevelChecker has been running for %s. terminating.",
258                         TimeUtil.formatElapsedTime(mMaxRunTime));
259                 break;
260             }
261 
262             getRunUtil().sleep((long) (mChargingPollTime * 60 * 1000));
263             Integer newLevel = checkBatteryLevel(mTestDevice);
264             if (newLevel == null) {
265                 // weird
266                 CLog.w("Breaking out of wait loop because battery level read failed for device %s",
267                         mTestDevice.getSerialNumber());
268                 listener.testFailed(
269                         test, FailureDescription.create("Failed to read battery level"));
270                 return null;
271             } else if (newLevel < batteryLevel) {
272                 // also weird
273                 CLog.w("Warning: battery discharged from %d to %d on device %s during the last " +
274                         "%.02f minutes.", batteryLevel, newLevel, mTestDevice.getSerialNumber(),
275                         mChargingPollTime);
276             } else {
277                 CLog.v("Battery level for device %s is currently %d", mTestDevice.getSerialNumber(),
278                         newLevel);
279             }
280             batteryLevel = newLevel;
281         }
282         return batteryLevel;
283     }
284 
checkChargingSpeed( ITestInvocationListener listener, Integer charge, long chargingTime)285     private void checkChargingSpeed(
286             ITestInvocationListener listener, Integer charge, long chargingTime) {
287         double speedPerHours = (charge / ((double) chargingTime / 3600));
288         if (speedPerHours < mChargingSpeedCheck) {
289             listener.testFailed(
290                     mChargingSpeed,
291                     FailureDescription.create(
292                             String.format(
293                                     "Device charged %s%% in %s = %s%%/hours. This is below %s",
294                                     charge,
295                                     TimeUtil.formatElapsedTime(chargingTime),
296                                     speedPerHours,
297                                     mChargingSpeedCheck)));
298             mTestDevice.logBugreport("low-charging-speed-bugreport", listener);
299         }
300         CLog.d("Device charged %s%% in %s", charge, TimeUtil.formatElapsedTime(chargingTime));
301     }
302 
303     /**
304      * Get a RunUtil instance
305      *
306      * <p>Exposed for unit testing
307      */
308     @VisibleForTesting
getRunUtil()309     IRunUtil getRunUtil() {
310         return RunUtil.getDefault();
311     }
312 
setResumeLevel(int level)313     protected void setResumeLevel(int level) {
314         mResumeLevel = level;
315     }
316 
317     @VisibleForTesting
getCurrentTimeMs()318     long getCurrentTimeMs() {
319         return System.currentTimeMillis();
320     }
321 }
322 
323