blob: 842f1021abb48b905a7a5c29c30271c92acad15b [file] [log] [blame]
/*
* Copyright (C) 2014 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.ddmlib;
import com.android.annotations.Nullable;
import com.google.common.util.concurrent.SettableFuture;
import java.io.IOException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Fetches battery level from device.
*/
class BatteryFetcher {
private static final String LOG_TAG = "BatteryFetcher";
/** the amount of time to wait between unsuccessful battery fetch attempts */
private static final long FETCH_BACKOFF_MS = 5 * 1000; // 5 seconds
private static final long BATTERY_TIMEOUT = 2 * 1000; // 2 seconds
/**
* Output receiver for "cat /sys/class/power_supply/.../capacity" command line.
*/
static final class SysFsBatteryLevelReceiver extends MultiLineReceiver {
private static final Pattern BATTERY_LEVEL = Pattern.compile("^(\\d+)[.\\s]*");
private Integer mBatteryLevel = null;
/**
* Get the parsed battery level.
* @return battery level or <code>null</code> if it cannot be determined
*/
@Nullable
public Integer getBatteryLevel() {
return mBatteryLevel;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
Matcher batteryMatch = BATTERY_LEVEL.matcher(line);
if (batteryMatch.matches()) {
if (mBatteryLevel == null) {
mBatteryLevel = Integer.parseInt(batteryMatch.group(1));
} else {
// multiple matches, check if they are different
Integer tmpLevel = Integer.parseInt(batteryMatch.group(1));
if (!mBatteryLevel.equals(tmpLevel)) {
Log.w(LOG_TAG, String.format(
"Multiple lines matched with different value; " +
"Original: %s, Current: %s (keeping original)",
mBatteryLevel.toString(), tmpLevel.toString()));
}
}
}
}
}
}
/**
* Output receiver for "dumpsys battery" command line.
*/
private static final class BatteryReceiver extends MultiLineReceiver {
private static final Pattern BATTERY_LEVEL = Pattern.compile("\\s*level: (\\d+)");
private static final Pattern SCALE = Pattern.compile("\\s*scale: (\\d+)");
private Integer mBatteryLevel = null;
private Integer mBatteryScale = null;
/**
* Get the parsed percent battery level.
* @return
*/
public Integer getBatteryLevel() {
if (mBatteryLevel != null && mBatteryScale != null) {
return (mBatteryLevel * 100) / mBatteryScale;
}
return null;
}
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
Matcher batteryMatch = BATTERY_LEVEL.matcher(line);
if (batteryMatch.matches()) {
try {
mBatteryLevel = Integer.parseInt(batteryMatch.group(1));
} catch (NumberFormatException e) {
Log.w(LOG_TAG, String.format("Failed to parse %s as an integer",
batteryMatch.group(1)));
}
}
Matcher scaleMatch = SCALE.matcher(line);
if (scaleMatch.matches()) {
try {
mBatteryScale = Integer.parseInt(scaleMatch.group(1));
} catch (NumberFormatException e) {
Log.w(LOG_TAG, String.format("Failed to parse %s as an integer",
batteryMatch.group(1)));
}
}
}
}
@Override
public boolean isCancelled() {
return false;
}
}
private Integer mBatteryLevel = null;
private final IDevice mDevice;
private long mLastSuccessTime = 0;
private SettableFuture<Integer> mPendingRequest = null;
public BatteryFetcher(IDevice device) {
mDevice = device;
}
/**
* Make a possibly asynchronous request for the device's battery level
*
* @param freshness the desired recentness of battery level
* @param timeUnit the {@link TimeUnit} of freshness
* @return a {@link Future} that can be used to retrieve the battery level
*/
public synchronized Future<Integer> getBattery(long freshness, TimeUnit timeUnit) {
SettableFuture<Integer> result;
if (mBatteryLevel == null || isFetchRequired(freshness, timeUnit)) {
if (mPendingRequest == null) {
// no request underway - start a new one
mPendingRequest = SettableFuture.create();
initiateBatteryQuery();
} else {
// fall through - return the already created future from the request already
// underway
}
result = mPendingRequest;
} else {
// cache is populated within desired freshness
result = SettableFuture.create();
result.set(mBatteryLevel);
}
return result;
}
private boolean isFetchRequired(long freshness, TimeUnit timeUnit) {
long freshnessMs = timeUnit.toMillis(freshness);
return (System.currentTimeMillis() - mLastSuccessTime) > freshnessMs;
}
private void initiateBatteryQuery() {
String threadName = String.format("query-battery-%s", mDevice.getSerialNumber());
Thread fetchThread = new Thread(threadName) {
@Override
public void run() {
Exception exception = null;
try {
// first try to get it from sysfs
SysFsBatteryLevelReceiver sysBattReceiver = new SysFsBatteryLevelReceiver();
mDevice.executeShellCommand("cat /sys/class/power_supply/*/capacity",
sysBattReceiver, BATTERY_TIMEOUT, TimeUnit.MILLISECONDS);
if (!setBatteryLevel(sysBattReceiver.getBatteryLevel())) {
// failed! try dumpsys
BatteryReceiver receiver = new BatteryReceiver();
mDevice.executeShellCommand("dumpsys battery", receiver, BATTERY_TIMEOUT,
TimeUnit.MILLISECONDS);
if (setBatteryLevel(receiver.getBatteryLevel())) {
return;
}
}
exception = new IOException("Unrecognized response to battery level queries");
} catch (TimeoutException e) {
exception = e;
} catch (AdbCommandRejectedException e) {
exception = e;
} catch (ShellCommandUnresponsiveException e) {
exception = e;
} catch (IOException e) {
exception = e;
}
handleBatteryLevelFailure(exception);
}
};
fetchThread.setDaemon(true);
fetchThread.start();
}
private synchronized boolean setBatteryLevel(Integer batteryLevel) {
if (batteryLevel == null) {
return false;
}
mLastSuccessTime = System.currentTimeMillis();
mBatteryLevel = batteryLevel;
if (mPendingRequest != null) {
mPendingRequest.set(mBatteryLevel);
}
mPendingRequest = null;
return true;
}
private synchronized void handleBatteryLevelFailure(Exception e) {
Log.w(LOG_TAG, String.format(
"%s getting battery level for device %s: %s",
e.getClass().getSimpleName(), mDevice.getSerialNumber(), e.getMessage()));
if (mPendingRequest != null) {
if (!mPendingRequest.setException(e)) {
// should never happen
Log.e(LOG_TAG, "Future.setException failed");
mPendingRequest.set(null);
}
}
mPendingRequest = null;
}
}