/* * 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; } }