Render enhanced battery projection curves.

BatteryInfo now supplies standard linear or enhanced projection curves,
depending on the provider.

Note that the semantics of parsing have changed slightly in that the
value of endTime supplied to onParsingStarted is now the end time of the
historical data and does not include the projection. However, as far as
I can see there is no existing code that depends on the parsing
functionality besides BatteryInfo itself.

Also slightly optimizes the updating behavior of the UsageGraph, since
we are now reconfiguring it multiple times.

Bug: 38400320
Test: make RunSettingsRoboTests, manual on device
Change-Id: Ieff26d31356b34bb38e49f54f979fd80549864b2
This commit is contained in:
Alex Kulesza
2017-06-29 12:26:59 -04:00
parent 43d4fefb36
commit 82dbcd973d
5 changed files with 196 additions and 35 deletions

View File

@@ -30,10 +30,10 @@ import android.text.format.Formatter;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import com.android.internal.os.BatteryStatsHelper; import com.android.internal.os.BatteryStatsHelper;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.R;
import com.android.settings.Utils; import com.android.settings.Utils;
import com.android.settings.graph.UsageView; import com.android.settings.graph.UsageView;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.R;
public class BatteryInfo { public class BatteryInfo {
@@ -54,18 +54,20 @@ public class BatteryInfo {
} }
public void bindHistory(final UsageView view, BatteryDataParser... parsers) { public void bindHistory(final UsageView view, BatteryDataParser... parsers) {
final Context context = view.getContext();
BatteryDataParser parser = new BatteryDataParser() { BatteryDataParser parser = new BatteryDataParser() {
SparseIntArray points = new SparseIntArray(); SparseIntArray points = new SparseIntArray();
long startTime;
int lastTime = -1; int lastTime = -1;
byte lastLevel; byte lastLevel;
int maxTime;
@Override @Override
public void onParsingStarted(long startTime, long endTime) { public void onParsingStarted(long startTime, long endTime) {
this.maxTime = (int) (endTime - startTime); this.startTime = startTime;
timePeriod = maxTime - (remainingTimeUs / 1000); timePeriod = endTime - startTime;
view.clearPaths(); view.clearPaths();
view.configureGraph(maxTime, 100); // Initially configure the graph for history only.
view.configureGraph((int) timePeriod, 100);
} }
@Override @Override
@@ -87,10 +89,27 @@ public class BatteryInfo {
public void onParsingDone() { public void onParsingDone() {
onDataGap(); onDataGap();
// Add linear projection // Add projection if we have an estimate.
if (lastTime >= 0 && remainingTimeUs != 0) { if (remainingTimeUs != 0) {
PowerUsageFeatureProvider provider = FeatureFactory.getFactory(context)
.getPowerUsageFeatureProvider(context);
if (!mCharging && provider.isEnhancedBatteryPredictionEnabled(context)) {
points = provider.getEnhancedBatteryPredictionCurve(context, startTime);
} else {
// Linear extrapolation.
if (lastTime >= 0) {
points.put(lastTime, lastLevel); points.put(lastTime, lastLevel);
points.put(maxTime, mCharging ? 100 : 0); points.put((int) (timePeriod +
BatteryUtils.convertUsToMs(remainingTimeUs)),
mCharging ? 100 : 0);
}
}
}
// If we have a projection, reconfigure the graph to show it.
if (points != null && points.size() > 0) {
int maxTime = points.keyAt(points.size() - 1);
view.configureGraph(maxTime, 100);
view.addProjectedPath(points); view.addProjectedPath(points);
} }
} }
@@ -100,8 +119,7 @@ public class BatteryInfo {
parserList[i] = parsers[i]; parserList[i] = parsers[i];
} }
parserList[parsers.length] = parser; parserList[parsers.length] = parser;
parse(mStats, remainingTimeUs, parserList); parse(mStats, parserList);
final Context context = view.getContext();
String timeString = context.getString(R.string.charge_length_format, String timeString = context.getString(R.string.charge_length_format,
Formatter.formatShortElapsedTime(context, timePeriod)); Formatter.formatShortElapsedTime(context, timePeriod));
String remaining = ""; String remaining = "";
@@ -249,14 +267,11 @@ public class BatteryInfo {
void onParsingDone(); void onParsingDone();
} }
private static void parse(BatteryStats stats, long remainingTimeUs, private static void parse(BatteryStats stats, BatteryDataParser... parsers) {
BatteryDataParser... parsers) {
long startWalltime = 0; long startWalltime = 0;
long endDateWalltime = 0;
long endWalltime = 0; long endWalltime = 0;
long historyStart = 0; long historyStart = 0;
long historyEnd = 0; long historyEnd = 0;
byte lastLevel = -1;
long curWalltime = startWalltime; long curWalltime = startWalltime;
long lastWallTime = 0; long lastWallTime = 0;
long lastRealtime = 0; long lastRealtime = 0;
@@ -292,17 +307,13 @@ public class BatteryInfo {
} }
} }
if (rec.isDeltaData()) { if (rec.isDeltaData()) {
if (rec.batteryLevel != lastLevel || pos == 1) {
lastLevel = rec.batteryLevel;
}
lastInteresting = pos; lastInteresting = pos;
historyEnd = rec.time; historyEnd = rec.time;
} }
} }
} }
stats.finishIteratingHistoryLocked(); stats.finishIteratingHistoryLocked();
endDateWalltime = lastWallTime + historyEnd - lastRealtime; endWalltime = lastWallTime + historyEnd - lastRealtime;
endWalltime = endDateWalltime + (remainingTimeUs / 1000);
int i = 0; int i = 0;
final int N = lastInteresting; final int N = lastInteresting;
@@ -310,7 +321,7 @@ public class BatteryInfo {
for (int j = 0; j < parsers.length; j++) { for (int j = 0; j < parsers.length; j++) {
parsers[j].onParsingStarted(startWalltime, endWalltime); parsers[j].onParsingStarted(startWalltime, endWalltime);
} }
if (endDateWalltime > startWalltime && stats.startIteratingHistoryLocked()) { if (endWalltime > startWalltime && stats.startIteratingHistoryLocked()) {
final HistoryItem rec = new HistoryItem(); final HistoryItem rec = new HistoryItem();
while (stats.getNextHistoryLocked(rec) && i < N) { while (stats.getNextHistoryLocked(rec) && i < N) {
if (rec.isDeltaData()) { if (rec.isDeltaData()) {

View File

@@ -20,8 +20,9 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.util.SparseIntArray;
import com.android.internal.os.BatterySipper; import com.android.internal.os.BatterySipper;
import com.android.settings.fuelgauge.anomaly.Anomaly;
/** /**
* Feature Provider used in power usage * Feature Provider used in power usage
@@ -67,6 +68,12 @@ public interface PowerUsageFeatureProvider {
*/ */
long getEnhancedBatteryPrediction(Context context); long getEnhancedBatteryPrediction(Context context);
/**
* Returns an improved projection curve for future battery level.
* @param zeroTime timestamps (array keys) are shifted by this amount
*/
SparseIntArray getEnhancedBatteryPredictionCurve(Context context, long zeroTime);
/** /**
* Checks whether the toggle for enhanced battery predictions is enabled. * Checks whether the toggle for enhanced battery predictions is enabled.
*/ */

View File

@@ -22,9 +22,10 @@ import android.content.pm.PackageManager;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Process; import android.os.Process;
import android.util.SparseIntArray;
import com.android.internal.os.BatterySipper; import com.android.internal.os.BatterySipper;
import com.android.internal.util.ArrayUtils; import com.android.internal.util.ArrayUtils;
import com.android.settings.fuelgauge.anomaly.Anomaly;
public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider { public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider {
@@ -93,6 +94,11 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider
return -1; return -1;
} }
@Override
public SparseIntArray getEnhancedBatteryPredictionCurve(Context context, long zeroTime) {
return null;
}
@Override @Override
public boolean isEnhancedBatteryPredictionEnabled(Context context) { public boolean isEnhancedBatteryPredictionEnabled(Context context) {
return false; return false;

View File

@@ -110,6 +110,8 @@ public class UsageGraph extends View {
void setMax(int maxX, int maxY) { void setMax(int maxX, int maxY) {
mMaxX = maxX; mMaxX = maxX;
mMaxY = maxY; mMaxY = maxY;
calculateLocalPaths();
postInvalidate();
} }
void setDividerLoc(int height) { void setDividerLoc(int height) {
@@ -151,6 +153,10 @@ public class UsageGraph extends View {
protected void onSizeChanged(int w, int h, int oldw, int oldh) { protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh); super.onSizeChanged(w, h, oldw, oldh);
updateGradient(); updateGradient();
calculateLocalPaths();
}
private void calculateLocalPaths() {
calculateLocalPaths(mPaths, mLocalPaths); calculateLocalPaths(mPaths, mLocalPaths);
calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths); calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
} }
@@ -222,9 +228,10 @@ public class UsageGraph extends View {
mMiddleDividerTint); mMiddleDividerTint);
drawDivider(canvas.getHeight() - mDividerSize, canvas, -1); drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
if (mLocalPaths.size() == 0 && mProjectedPaths.size() == 0) { if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) {
return; return;
} }
drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint); drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
drawFilledPath(canvas, mLocalPaths, mFillPaint); drawFilledPath(canvas, mLocalPaths, mFillPaint);
drawLinePath(canvas, mLocalPaths, mLinePaint); drawLinePath(canvas, mLocalPaths, mLinePaint);

View File

@@ -16,34 +16,45 @@
package com.android.settings.fuelgauge; package com.android.settings.fuelgauge;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.os.BatteryStats; import android.os.BatteryStats;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.SparseIntArray;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig; import com.android.settings.TestConfig;
import com.android.settings.graph.UsageView;
import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import java.util.concurrent.TimeUnit;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Answers; import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import static com.google.common.truth.Truth.assertThat; import java.util.concurrent.TimeUnit;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
@RunWith(SettingsRobolectricTestRunner.class) @RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
@@ -62,6 +73,7 @@ public class BatteryInfoTest {
private Intent mDisChargingBatteryBroadcast; private Intent mDisChargingBatteryBroadcast;
private Intent mChargingBatteryBroadcast; private Intent mChargingBatteryBroadcast;
private Context mContext; private Context mContext;
private FakeFeatureFactory mFeatureFactory;
@Mock(answer = Answers.RETURNS_DEEP_STUBS) @Mock(answer = Answers.RETURNS_DEEP_STUBS)
private BatteryStats mBatteryStats; private BatteryStats mBatteryStats;
@@ -73,7 +85,7 @@ public class BatteryInfoTest {
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application); mContext = spy(RuntimeEnvironment.application);
FakeFeatureFactory.setupForTest(mContext); mFeatureFactory = FakeFeatureFactory.setupForTest(mContext);
mDisChargingBatteryBroadcast = new Intent(); mDisChargingBatteryBroadcast = new Intent();
mDisChargingBatteryBroadcast.putExtra(BatteryManager.EXTRA_PLUGGED, 0); mDisChargingBatteryBroadcast.putExtra(BatteryManager.EXTRA_PLUGGED, 0);
@@ -175,4 +187,122 @@ public class BatteryInfoTest {
assertThat(info.chargeLabel).isEqualTo("100%"); assertThat(info.chargeLabel).isEqualTo("100%");
} }
// Make our battery stats return a sequence of battery events.
private void mockBatteryStatsHistory() {
// Mock out new data every time start...Locked is called.
doAnswer(invocation -> {
doAnswer(new Answer() {
private int count = 0;
private long[] times = {1000, 1500, 2000};
private byte[] levels = {99, 98, 97};
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
if (count == times.length) {
return false;
}
BatteryStats.HistoryItem record = invocation.getArgument(0);
record.cmd = BatteryStats.HistoryItem.CMD_UPDATE;
record.time = times[count];
record.batteryLevel = levels[count];
count++;
return true;
}
}).when(mBatteryStats).getNextHistoryLocked(any(BatteryStats.HistoryItem.class));
return true;
}).when(mBatteryStats).startIteratingHistoryLocked();
}
private void assertOnlyHistory(BatteryInfo info) {
mockBatteryStatsHistory();
UsageView view = mock(UsageView.class);
doReturn(mContext).when(view).getContext();
info.bindHistory(view);
verify(view, times(1)).configureGraph(anyInt(), anyInt());
verify(view, times(1)).addPath(any(SparseIntArray.class));
verify(view, never()).addProjectedPath(any(SparseIntArray.class));
}
private void assertHistoryAndLinearProjection(BatteryInfo info) {
mockBatteryStatsHistory();
UsageView view = mock(UsageView.class);
doReturn(mContext).when(view).getContext();
info.bindHistory(view);
verify(view, times(2)).configureGraph(anyInt(), anyInt());
verify(view, times(1)).addPath(any(SparseIntArray.class));
ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class);
verify(view, times(1)).addProjectedPath(pointsActual.capture());
// Check that we have two points and the first is correct.
assertThat(pointsActual.getValue().size()).isEqualTo(2);
assertThat(pointsActual.getValue().keyAt(0)).isEqualTo(2000);
assertThat(pointsActual.getValue().valueAt(0)).isEqualTo(97);
}
private void assertHistoryAndEnhancedProjection(BatteryInfo info) {
mockBatteryStatsHistory();
UsageView view = mock(UsageView.class);
doReturn(mContext).when(view).getContext();
SparseIntArray pointsExpected = new SparseIntArray();
pointsExpected.append(2000, 96);
pointsExpected.append(2500, 95);
pointsExpected.append(3000, 94);
doReturn(pointsExpected).when(mFeatureFactory.powerUsageFeatureProvider)
.getEnhancedBatteryPredictionCurve(any(Context.class), anyLong());
info.bindHistory(view);
verify(view, times(2)).configureGraph(anyInt(), anyInt());
verify(view, times(1)).addPath(any(SparseIntArray.class));
ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class);
verify(view, times(1)).addProjectedPath(pointsActual.capture());
assertThat(pointsActual.getValue()).isEqualTo(pointsExpected);
}
private BatteryInfo getBatteryInfo(boolean charging, boolean enhanced, boolean estimate) {
if (charging && estimate) {
doReturn(1000L).when(mBatteryStats).computeChargeTimeRemaining(anyLong());
} else {
doReturn(0L).when(mBatteryStats).computeChargeTimeRemaining(anyLong());
}
BatteryInfo info = BatteryInfo.getBatteryInfo(mContext,
charging ? mChargingBatteryBroadcast : mDisChargingBatteryBroadcast,
mBatteryStats, SystemClock.elapsedRealtime() * 1000, false,
estimate ? 1000 : 0 /* drainTimeUs */, false);
doReturn(enhanced).when(mFeatureFactory.powerUsageFeatureProvider)
.isEnhancedBatteryPredictionEnabled(mContext);
return info;
}
@Test
public void testBindHistory() {
BatteryInfo info;
info = getBatteryInfo(false /* charging */, false /* enhanced */, false /* estimate */);
assertOnlyHistory(info);
info = getBatteryInfo(false /* charging */, false /* enhanced */, true /* estimate */);
assertHistoryAndLinearProjection(info);
info = getBatteryInfo(false /* charging */, true /* enhanced */, false /* estimate */);
assertOnlyHistory(info);
info = getBatteryInfo(false /* charging */, true /* enhanced */, true /* estimate */);
assertHistoryAndEnhancedProjection(info);
info = getBatteryInfo(true /* charging */, false /* enhanced */, false /* estimate */);
assertOnlyHistory(info);
info = getBatteryInfo(true /* charging */, false /* enhanced */, true /* estimate */);
assertHistoryAndLinearProjection(info);
info = getBatteryInfo(true /* charging */, true /* enhanced */, false /* estimate */);
assertOnlyHistory(info);
// Linear projection for charging even in enhanced mode.
info = getBatteryInfo(true /* charging */, true /* enhanced */, true /* estimate */);
assertHistoryAndLinearProjection(info);
}
} }