Create a TargetPreparer that holds a device for charging if needed

Change-Id: I4119934db11b63339da618fe32170a8152ab8060
diff --git a/src/com/android/tradefed/targetprep/DeviceBatteryLevelChecker.java b/src/com/android/tradefed/targetprep/DeviceBatteryLevelChecker.java
new file mode 100644
index 0000000..06e6a7f
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/DeviceBatteryLevelChecker.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * An {@link ITargetPreparer} that checks for a minimum battery charge, and waits for the battery
+ * to reach a second charging threshold if the minimum charge isn't present.
+ */
+@OptionClass(alias = "battery-checker")
+public class DeviceBatteryLevelChecker implements ITargetPreparer {
+
+    @Option(name="min-level", description="Charge level below which we force the device to sit " +
+            "and charge.  Range: 0-100.")
+    private int mMinChargeLevel = 20;
+
+    @Option(name="resume-level", description="Charge level at which we release the device to " +
+            "begin testing again. Range: 0-100.")
+    private int mResumeLevel = 80;
+
+    private static final Pattern BATTERY_LEVEL = Pattern.compile("\\s*level: (\\d+)");
+    /** poll the battery level every 5 minutes while the device is charging */
+    private static final long CHARGING_POLL_TIME = 5 * 60 * 1000;
+
+    private Integer checkBatteryLevel(ITestDevice device) throws DeviceNotAvailableException {
+        // FIXME: scale the battery level by "scale" instead of assuming 100
+        String dumpsys = device.executeShellCommand("dumpsys battery");
+        if (dumpsys != null) {
+            String[] lines = dumpsys.split("\r?\n");
+            for (String line : lines) {
+                Matcher m = BATTERY_LEVEL.matcher(line);
+                if (m.matches()) {
+                    try {
+                        return Integer.parseInt(m.group(1));
+                    } catch (NumberFormatException e) {
+                        CLog.w("Failed to parse %s as an integer", m.group(1));
+                    }
+                }
+            }
+        }
+        CLog.w("Failed to determine battery level for device %s.  `dumpsys battery` was: %s",
+                device.getSerialNumber(), dumpsys);
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, BuildError,
+            DeviceNotAvailableException {
+        Integer batteryLevel = checkBatteryLevel(device);
+        if (batteryLevel == null) {
+            // we already logged a warning
+            return;
+        } else if (batteryLevel < mMinChargeLevel) {
+            // Time-out.  Send the device to the corner
+            CLog.w("Battery level %d is below the min level %d; holding for device %s to charge " +
+                    "to level %d", batteryLevel, mMinChargeLevel, device.getSerialNumber(),
+                    mResumeLevel);
+        } else {
+            // Good to go
+            CLog.d("Battery level %d is above the minimum of %d; %s is good to go.", batteryLevel,
+                    mMinChargeLevel, device.getSerialNumber());
+            return;
+        }
+
+        // If we're down here, it's time to hold the device until it reaches mResumeLevel
+        while (batteryLevel != null && batteryLevel < mResumeLevel) {
+            // FIXME show periodic status messages with "w" log level
+            getRunUtil().sleep(CHARGING_POLL_TIME);
+            Integer newLevel = checkBatteryLevel(device);
+            CLog.d("Battery level for device %s is now %d", device.getSerialNumber(), newLevel);
+            batteryLevel = newLevel;
+        }
+        CLog.w("Device %s is now charged to battery level %d; releasing.", device.getSerialNumber(),
+                batteryLevel);
+    }
+
+    /**
+     * Get a RunUtil instance
+     * <p />
+     * Exposed for unit testing
+     */
+    IRunUtil getRunUtil() {
+        return RunUtil.getDefault();
+    }
+}
+
diff --git a/tests/src/com/android/tradefed/util/DeviceBatteryLevelCheckerTest.java b/tests/src/com/android/tradefed/util/DeviceBatteryLevelCheckerTest.java
new file mode 100644
index 0000000..2245f24
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/DeviceBatteryLevelCheckerTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.IRunUtil;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+
+public class DeviceBatteryLevelCheckerTest extends TestCase {
+    private DeviceBatteryLevelChecker mChecker = null;
+    ITestDevice mFakeDevice = null;
+
+    private static final String BATTERY_TEMPLATE = "Current Battery Service state:\n" +
+            "  AC powered: false\n" +
+            "  USB powered: true\n" +
+            "  status: 2\n" +
+            "  health: 2\n" +
+            "  present: true\n" +
+            "  level: %d\n" +
+            "  scale: 100\n" +
+            "  voltage:3400\n" +
+            "  temperature: 250\n" +
+            "  technology: Li-ion\n";
+
+    private String battLevelString(int level) {
+        return String.format(BATTERY_TEMPLATE, level);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mChecker = new DeviceBatteryLevelChecker() {
+            @Override
+            IRunUtil getRunUtil() {
+                return EasyMock.createNiceMock(IRunUtil.class);
+            }
+        };
+        mFakeDevice = EasyMock.createStrictMock(ITestDevice.class);
+        EasyMock.expect(mFakeDevice.getSerialNumber()).andStubReturn("SERIAL");
+    }
+
+    public void testNull() throws Exception {
+        EasyMock.expect(mFakeDevice.executeShellCommand("dumpsys battery"))
+                .andReturn(null);
+        EasyMock.replay(mFakeDevice);
+
+        mChecker.setUp(mFakeDevice, null);
+        // expect this to return immediately without throwing an exception.  Should log a warning.
+        EasyMock.verify(mFakeDevice);
+    }
+
+    public void testNormal() throws Exception {
+        EasyMock.expect(mFakeDevice.executeShellCommand("dumpsys battery"))
+                .andReturn(battLevelString(45));
+        EasyMock.replay(mFakeDevice);
+
+        mChecker.setUp(mFakeDevice, null);
+        EasyMock.verify(mFakeDevice);
+    }
+
+    public void testLow() throws Exception {
+        EasyMock.expect(mFakeDevice.executeShellCommand("dumpsys battery"))
+                .andReturn(battLevelString(15));
+        EasyMock.expect(mFakeDevice.executeShellCommand("dumpsys battery"))
+                .andReturn(battLevelString(20));
+        EasyMock.expect(mFakeDevice.executeShellCommand("dumpsys battery"))
+                .andReturn(battLevelString(50));
+        EasyMock.expect(mFakeDevice.executeShellCommand("dumpsys battery"))
+                .andReturn(battLevelString(90));
+        EasyMock.replay(mFakeDevice);
+
+        mChecker.setUp(mFakeDevice, null);
+        EasyMock.verify(mFakeDevice);
+    }
+}
+