diff --git a/src/com/android/settings/fuelgauge/BatteryInfo.java b/src/com/android/settings/fuelgauge/BatteryInfo.java index f85ce41c14e..0a9cfd3a4f5 100644 --- a/src/com/android/settings/fuelgauge/BatteryInfo.java +++ b/src/com/android/settings/fuelgauge/BatteryInfo.java @@ -30,10 +30,10 @@ import android.text.format.Formatter; import android.util.SparseIntArray; 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.graph.UsageView; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.R; public class BatteryInfo { @@ -54,18 +54,20 @@ public class BatteryInfo { } public void bindHistory(final UsageView view, BatteryDataParser... parsers) { + final Context context = view.getContext(); BatteryDataParser parser = new BatteryDataParser() { SparseIntArray points = new SparseIntArray(); + long startTime; int lastTime = -1; byte lastLevel; - int maxTime; @Override public void onParsingStarted(long startTime, long endTime) { - this.maxTime = (int) (endTime - startTime); - timePeriod = maxTime - (remainingTimeUs / 1000); + this.startTime = startTime; + timePeriod = endTime - startTime; view.clearPaths(); - view.configureGraph(maxTime, 100); + // Initially configure the graph for history only. + view.configureGraph((int) timePeriod, 100); } @Override @@ -87,10 +89,27 @@ public class BatteryInfo { public void onParsingDone() { onDataGap(); - // Add linear projection - if (lastTime >= 0 && remainingTimeUs != 0) { - points.put(lastTime, lastLevel); - points.put(maxTime, mCharging ? 100 : 0); + // Add projection if we have an estimate. + 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((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); } } @@ -100,8 +119,7 @@ public class BatteryInfo { parserList[i] = parsers[i]; } parserList[parsers.length] = parser; - parse(mStats, remainingTimeUs, parserList); - final Context context = view.getContext(); + parse(mStats, parserList); String timeString = context.getString(R.string.charge_length_format, Formatter.formatShortElapsedTime(context, timePeriod)); String remaining = ""; @@ -249,14 +267,11 @@ public class BatteryInfo { void onParsingDone(); } - private static void parse(BatteryStats stats, long remainingTimeUs, - BatteryDataParser... parsers) { + private static void parse(BatteryStats stats, BatteryDataParser... parsers) { long startWalltime = 0; - long endDateWalltime = 0; long endWalltime = 0; long historyStart = 0; long historyEnd = 0; - byte lastLevel = -1; long curWalltime = startWalltime; long lastWallTime = 0; long lastRealtime = 0; @@ -292,17 +307,13 @@ public class BatteryInfo { } } if (rec.isDeltaData()) { - if (rec.batteryLevel != lastLevel || pos == 1) { - lastLevel = rec.batteryLevel; - } lastInteresting = pos; historyEnd = rec.time; } } } stats.finishIteratingHistoryLocked(); - endDateWalltime = lastWallTime + historyEnd - lastRealtime; - endWalltime = endDateWalltime + (remainingTimeUs / 1000); + endWalltime = lastWallTime + historyEnd - lastRealtime; int i = 0; final int N = lastInteresting; @@ -310,7 +321,7 @@ public class BatteryInfo { for (int j = 0; j < parsers.length; j++) { parsers[j].onParsingStarted(startWalltime, endWalltime); } - if (endDateWalltime > startWalltime && stats.startIteratingHistoryLocked()) { + if (endWalltime > startWalltime && stats.startIteratingHistoryLocked()) { final HistoryItem rec = new HistoryItem(); while (stats.getNextHistoryLocked(rec) && i < N) { if (rec.isDeltaData()) { diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java index 969afb98681..6d7e1e8dac3 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java @@ -20,8 +20,9 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; +import android.util.SparseIntArray; + import com.android.internal.os.BatterySipper; -import com.android.settings.fuelgauge.anomaly.Anomaly; /** * Feature Provider used in power usage @@ -67,6 +68,12 @@ public interface PowerUsageFeatureProvider { */ 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. */ diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java index df7e7e1e790..08143645ef7 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java @@ -22,9 +22,10 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Process; +import android.util.SparseIntArray; + import com.android.internal.os.BatterySipper; import com.android.internal.util.ArrayUtils; -import com.android.settings.fuelgauge.anomaly.Anomaly; public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider { @@ -93,6 +94,11 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider return -1; } + @Override + public SparseIntArray getEnhancedBatteryPredictionCurve(Context context, long zeroTime) { + return null; + } + @Override public boolean isEnhancedBatteryPredictionEnabled(Context context) { return false; diff --git a/src/com/android/settings/graph/UsageGraph.java b/src/com/android/settings/graph/UsageGraph.java index ccee89a255e..b4c4501097c 100644 --- a/src/com/android/settings/graph/UsageGraph.java +++ b/src/com/android/settings/graph/UsageGraph.java @@ -110,6 +110,8 @@ public class UsageGraph extends View { void setMax(int maxX, int maxY) { mMaxX = maxX; mMaxY = maxY; + calculateLocalPaths(); + postInvalidate(); } void setDividerLoc(int height) { @@ -151,6 +153,10 @@ public class UsageGraph extends View { protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); updateGradient(); + calculateLocalPaths(); + } + + private void calculateLocalPaths() { calculateLocalPaths(mPaths, mLocalPaths); calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths); } @@ -222,9 +228,10 @@ public class UsageGraph extends View { mMiddleDividerTint); drawDivider(canvas.getHeight() - mDividerSize, canvas, -1); - if (mLocalPaths.size() == 0 && mProjectedPaths.size() == 0) { + if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) { return; } + drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint); drawFilledPath(canvas, mLocalPaths, mFillPaint); drawLinePath(canvas, mLocalPaths, mLinePaint); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java index bb0421bc5b6..24d1bf9fd76 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java @@ -16,34 +16,45 @@ 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.Intent; import android.content.res.Resources; import android.os.BatteryManager; import android.os.BatteryStats; import android.os.SystemClock; +import android.util.SparseIntArray; -import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; +import com.android.settings.graph.UsageView; import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.SettingsRobolectricTestRunner; -import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import static com.google.common.truth.Truth.assertThat; - -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; +import java.util.concurrent.TimeUnit; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) @@ -62,6 +73,7 @@ public class BatteryInfoTest { private Intent mDisChargingBatteryBroadcast; private Intent mChargingBatteryBroadcast; private Context mContext; + private FakeFeatureFactory mFeatureFactory; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private BatteryStats mBatteryStats; @@ -73,7 +85,7 @@ public class BatteryInfoTest { public void setUp() { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); - FakeFeatureFactory.setupForTest(mContext); + mFeatureFactory = FakeFeatureFactory.setupForTest(mContext); mDisChargingBatteryBroadcast = new Intent(); mDisChargingBatteryBroadcast.putExtra(BatteryManager.EXTRA_PLUGGED, 0); @@ -175,4 +187,122 @@ public class BatteryInfoTest { 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 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 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); + } }