2 * Copyright (C) 2017 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.settings.fuelgauge;
19 import static com.google.common.truth.Truth.assertThat;
21 import static org.mockito.ArgumentMatchers.anyInt;
22 import static org.mockito.ArgumentMatchers.anyLong;
23 import static org.mockito.Mockito.any;
24 import static org.mockito.Mockito.doAnswer;
25 import static org.mockito.Mockito.doReturn;
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.never;
28 import static org.mockito.Mockito.spy;
29 import static org.mockito.Mockito.times;
30 import static org.mockito.Mockito.verify;
31 import static org.mockito.Mockito.when;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.os.BatteryManager;
36 import android.os.BatteryStats;
37 import android.os.SystemClock;
38 import android.util.SparseIntArray;
40 import com.android.settings.testutils.BatteryTestUtils;
41 import com.android.settings.testutils.FakeFeatureFactory;
42 import com.android.settings.widget.UsageView;
43 import com.android.settingslib.R;
45 import org.junit.Before;
46 import org.junit.Test;
47 import org.junit.runner.RunWith;
48 import org.mockito.Answers;
49 import org.mockito.ArgumentCaptor;
50 import org.mockito.Mock;
51 import org.mockito.MockitoAnnotations;
52 import org.mockito.invocation.InvocationOnMock;
53 import org.mockito.stubbing.Answer;
54 import org.robolectric.RobolectricTestRunner;
55 import org.robolectric.RuntimeEnvironment;
57 import java.time.Duration;
58 import java.util.concurrent.TimeUnit;
60 @RunWith(RobolectricTestRunner.class)
61 public class BatteryInfoTest {
63 private static final String STATUS_CHARGING_NO_TIME = "50% - charging";
64 private static final String STATUS_CHARGING_TIME = "50% - 0 min until fully charged";
65 private static final String STATUS_NOT_CHARGING = "Not charging";
66 private static final long REMAINING_TIME_NULL = -1;
67 private static final long REMAINING_TIME = 2;
68 // Strings are defined in frameworks/base/packages/SettingsLib/res/values/strings.xml
69 private static final String ENHANCED_STRING_SUFFIX = "based on your usage";
70 private static final String EXTEND_PREFIX = "Extend battery life past";
71 private static final long TEST_CHARGE_TIME_REMAINING = TimeUnit.MINUTES.toMicros(1);
72 private static final String TEST_CHARGE_TIME_REMAINING_STRINGIFIED =
73 "1 min left until fully charged";
74 private static final String TEST_BATTERY_LEVEL_10 = "10%";
75 private static final String FIFTEEN_MIN_FORMATTED = "15 min";
76 private static final Estimate DUMMY_ESTIMATE = new Estimate(
77 1000, /* estimateMillis */
78 false, /* isBasedOnUsage */
79 1000 /* averageDischargeTime */);
81 private Intent mDisChargingBatteryBroadcast;
82 private Intent mChargingBatteryBroadcast;
83 private Context mContext;
84 private FakeFeatureFactory mFeatureFactory;
86 @Mock(answer = Answers.RETURNS_DEEP_STUBS)
87 private BatteryStats mBatteryStats;
91 MockitoAnnotations.initMocks(this);
92 mContext = spy(RuntimeEnvironment.application);
93 mFeatureFactory = FakeFeatureFactory.setupForTest();
95 mDisChargingBatteryBroadcast = BatteryTestUtils.getDischargingIntent();
97 mChargingBatteryBroadcast = BatteryTestUtils.getChargingIntent();
101 public void testGetBatteryInfo_hasStatusLabel() {
102 doReturn(REMAINING_TIME_NULL).when(mBatteryStats).computeBatteryTimeRemaining(anyLong());
103 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext,
104 mDisChargingBatteryBroadcast, mBatteryStats, SystemClock.elapsedRealtime() * 1000,
105 true /* shortString */);
107 assertThat(info.statusLabel).isEqualTo(STATUS_NOT_CHARGING);
111 public void testGetBatteryInfo_doNotShowChargingMethod_hasRemainingTime() {
112 doReturn(REMAINING_TIME).when(mBatteryStats).computeChargeTimeRemaining(anyLong());
113 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast,
114 mBatteryStats, SystemClock.elapsedRealtime() * 1000, false /* shortString */);
116 assertThat(info.chargeLabel.toString()).isEqualTo(STATUS_CHARGING_TIME);
120 public void testGetBatteryInfo_doNotShowChargingMethod_noRemainingTime() {
121 doReturn(REMAINING_TIME_NULL).when(mBatteryStats).computeChargeTimeRemaining(anyLong());
122 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast,
123 mBatteryStats, SystemClock.elapsedRealtime() * 1000, false /* shortString */);
125 assertThat(info.chargeLabel.toString()).isEqualTo(STATUS_CHARGING_NO_TIME);
129 public void testGetBatteryInfo_pluggedInUsingShortString_usesCorrectData() {
130 doReturn(TEST_CHARGE_TIME_REMAINING).when(mBatteryStats).computeChargeTimeRemaining(
132 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast,
133 mBatteryStats, SystemClock.elapsedRealtime() * 1000, true /* shortString */);
135 assertThat(info.discharging).isEqualTo(false);
136 assertThat(info.chargeLabel.toString()).isEqualTo("50% - 1 min until fully charged");
140 public void testGetBatteryInfo_basedOnUsageTrueMoreThanFifteenMinutes_usesCorrectString() {
141 Estimate estimate = new Estimate(Duration.ofHours(4).toMillis(),
142 true /* isBasedOnUsage */,
143 1000 /* averageDischargeTime */);
144 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
145 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000,
146 false /* shortString */);
147 BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
148 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000,
149 true /* shortString */);
151 // We only add special mention for the long string
152 assertThat(info.remainingLabel.toString()).contains(ENHANCED_STRING_SUFFIX);
153 assertThat(info.suggestionLabel).contains(EXTEND_PREFIX);
154 // shortened string should not have extra text
155 assertThat(info2.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
156 assertThat(info2.suggestionLabel).contains(EXTEND_PREFIX);
160 public void testGetBatteryInfo_basedOnUsageTrueLessThanSevenMinutes_usesCorrectString() {
161 Estimate estimate = new Estimate(Duration.ofMinutes(7).toMillis(),
162 true /* isBasedOnUsage */,
163 1000 /* averageDischargeTime */);
164 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
165 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000,
166 false /* shortString */);
167 BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
168 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000,
169 true /* shortString */);
171 // These should be identical in either case
172 assertThat(info.remainingLabel.toString()).isEqualTo(
173 mContext.getString(R.string.power_remaining_duration_only_shutdown_imminent));
174 assertThat(info2.remainingLabel.toString()).isEqualTo(
175 mContext.getString(R.string.power_remaining_duration_only_shutdown_imminent));
176 assertThat(info2.suggestionLabel).contains(EXTEND_PREFIX);
180 public void getBatteryInfo_MoreThanOneDay_suggestionLabelIsCorrectString() {
181 Estimate estimate = new Estimate(Duration.ofDays(3).toMillis(),
182 true /* isBasedOnUsage */,
183 1000 /* averageDischargeTime */);
184 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
185 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000,
186 false /* shortString */);
188 assertThat(info.suggestionLabel).doesNotContain(EXTEND_PREFIX);
193 testGetBatteryInfo_basedOnUsageTrueBetweenSevenAndFifteenMinutes_usesCorrectString() {
194 Estimate estimate = new Estimate(Duration.ofMinutes(10).toMillis(),
195 true /* isBasedOnUsage */,
196 1000 /* averageDischargeTime */);
197 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
198 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000,
199 false /* shortString */);
201 // Check that strings are showing less than 15 minutes remaining regardless of exact time.
202 assertThat(info.chargeLabel.toString()).isEqualTo(
203 mContext.getString(R.string.power_remaining_less_than_duration,
204 FIFTEEN_MIN_FORMATTED, TEST_BATTERY_LEVEL_10));
205 assertThat(info.remainingLabel.toString()).isEqualTo(
206 mContext.getString(R.string.power_remaining_less_than_duration_only,
207 FIFTEEN_MIN_FORMATTED));
211 public void testGetBatteryInfo_basedOnUsageFalse_usesDefaultString() {
212 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
213 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000,
214 false /* shortString */);
215 BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
216 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000,
217 true /* shortString */);
219 assertThat(info.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
220 assertThat(info2.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
224 public void testGetBatteryInfo_charging_usesChargeTime() {
225 doReturn(TEST_CHARGE_TIME_REMAINING)
227 .computeChargeTimeRemaining(anyLong());
229 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mChargingBatteryBroadcast,
230 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000,
231 false /* shortString */);
232 assertThat(info.remainingTimeUs).isEqualTo(TEST_CHARGE_TIME_REMAINING);
233 assertThat(info.remainingLabel.toString())
234 .isEqualTo(TEST_CHARGE_TIME_REMAINING_STRINGIFIED);
238 public void testGetBatteryInfo_pluggedInWithFullBattery_onlyShowBatteryLevel() {
239 mChargingBatteryBroadcast.putExtra(BatteryManager.EXTRA_LEVEL, 100);
241 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mChargingBatteryBroadcast,
242 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000,
243 false /* shortString */);
245 assertThat(info.chargeLabel).isEqualTo("100%");
248 // Make our battery stats return a sequence of battery events.
249 private void mockBatteryStatsHistory() {
250 // Mock out new data every time start...Locked is called.
251 doAnswer(invocation -> {
252 doAnswer(new Answer() {
253 private int count = 0;
254 private long[] times = {1000, 1500, 2000};
255 private byte[] levels = {99, 98, 97};
258 public Object answer(InvocationOnMock invocation) throws Throwable {
259 if (count == times.length) {
262 BatteryStats.HistoryItem record = invocation.getArgument(0);
263 record.cmd = BatteryStats.HistoryItem.CMD_UPDATE;
264 record.time = times[count];
265 record.batteryLevel = levels[count];
269 }).when(mBatteryStats).getNextHistoryLocked(any(BatteryStats.HistoryItem.class));
271 }).when(mBatteryStats).startIteratingHistoryLocked();
274 private void assertOnlyHistory(BatteryInfo info) {
275 mockBatteryStatsHistory();
276 UsageView view = mock(UsageView.class);
277 when(view.getContext()).thenReturn(mContext);
279 info.bindHistory(view);
280 verify(view, times(1)).configureGraph(anyInt(), anyInt());
281 verify(view, times(1)).addPath(any(SparseIntArray.class));
282 verify(view, never()).addProjectedPath(any(SparseIntArray.class));
285 private void assertHistoryAndLinearProjection(BatteryInfo info) {
286 mockBatteryStatsHistory();
287 UsageView view = mock(UsageView.class);
288 when(view.getContext()).thenReturn(mContext);
290 info.bindHistory(view);
291 verify(view, times(2)).configureGraph(anyInt(), anyInt());
292 verify(view, times(1)).addPath(any(SparseIntArray.class));
293 ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class);
294 verify(view, times(1)).addProjectedPath(pointsActual.capture());
296 // Check that we have two points and the first is correct.
297 assertThat(pointsActual.getValue().size()).isEqualTo(2);
298 assertThat(pointsActual.getValue().keyAt(0)).isEqualTo(2000);
299 assertThat(pointsActual.getValue().valueAt(0)).isEqualTo(97);
302 private void assertHistoryAndEnhancedProjection(BatteryInfo info) {
303 mockBatteryStatsHistory();
304 UsageView view = mock(UsageView.class);
305 when(view.getContext()).thenReturn(mContext);
306 SparseIntArray pointsExpected = new SparseIntArray();
307 pointsExpected.append(2000, 96);
308 pointsExpected.append(2500, 95);
309 pointsExpected.append(3000, 94);
310 doReturn(pointsExpected).when(mFeatureFactory.powerUsageFeatureProvider)
311 .getEnhancedBatteryPredictionCurve(any(Context.class), anyLong());
313 info.bindHistory(view);
314 verify(view, times(2)).configureGraph(anyInt(), anyInt());
315 verify(view, times(1)).addPath(any(SparseIntArray.class));
316 ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class);
317 verify(view, times(1)).addProjectedPath(pointsActual.capture());
318 assertThat(pointsActual.getValue()).isEqualTo(pointsExpected);
321 private BatteryInfo getBatteryInfo(boolean charging, boolean enhanced, boolean estimate) {
322 if (charging && estimate) {
323 doReturn(1000L).when(mBatteryStats).computeChargeTimeRemaining(anyLong());
325 doReturn(0L).when(mBatteryStats).computeChargeTimeRemaining(anyLong());
327 Estimate batteryEstimate = new Estimate(
329 false /* isBasedOnUsage */,
330 1000 /* averageDischargeTime */);
331 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext,
332 charging ? mChargingBatteryBroadcast : mDisChargingBatteryBroadcast,
333 mBatteryStats, batteryEstimate, SystemClock.elapsedRealtime() * 1000, false);
334 doReturn(enhanced).when(mFeatureFactory.powerUsageFeatureProvider)
335 .isEnhancedBatteryPredictionEnabled(mContext);
340 public void testBindHistory() {
343 info = getBatteryInfo(false /* charging */, false /* enhanced */, false /* estimate */);
344 assertOnlyHistory(info);
346 info = getBatteryInfo(false /* charging */, false /* enhanced */, true /* estimate */);
347 assertHistoryAndLinearProjection(info);
349 info = getBatteryInfo(false /* charging */, true /* enhanced */, false /* estimate */);
350 assertOnlyHistory(info);
352 info = getBatteryInfo(false /* charging */, true /* enhanced */, true /* estimate */);
353 assertHistoryAndEnhancedProjection(info);
355 info = getBatteryInfo(true /* charging */, false /* enhanced */, false /* estimate */);
356 assertOnlyHistory(info);
358 info = getBatteryInfo(true /* charging */, false /* enhanced */, true /* estimate */);
359 assertHistoryAndLinearProjection(info);
361 info = getBatteryInfo(true /* charging */, true /* enhanced */, false /* estimate */);
362 assertOnlyHistory(info);
364 // Linear projection for charging even in enhanced mode.
365 info = getBatteryInfo(true /* charging */, true /* enhanced */, true /* estimate */);
366 assertHistoryAndLinearProjection(info);