1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 
17 package com.android.settings.fuelgauge;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
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;
32 
33 import android.content.Context;
34 import android.content.Intent;
35 import android.os.BatteryManager;
36 import android.os.BatteryStats;
37 import android.os.BatteryUsageStats;
38 import android.os.SystemClock;
39 import android.util.SparseIntArray;
40 
41 import com.android.internal.os.BatteryStatsHistoryIterator;
42 import com.android.settings.testutils.BatteryTestUtils;
43 import com.android.settings.testutils.FakeFeatureFactory;
44 import com.android.settings.widget.UsageView;
45 import com.android.settingslib.R;
46 import com.android.settingslib.fuelgauge.Estimate;
47 
48 import org.junit.Before;
49 import org.junit.Test;
50 import org.junit.runner.RunWith;
51 import org.mockito.ArgumentCaptor;
52 import org.mockito.Mock;
53 import org.mockito.MockitoAnnotations;
54 import org.mockito.invocation.InvocationOnMock;
55 import org.mockito.stubbing.Answer;
56 import org.robolectric.RobolectricTestRunner;
57 import org.robolectric.RuntimeEnvironment;
58 
59 import java.time.Duration;
60 import java.util.concurrent.TimeUnit;
61 
62 @RunWith(RobolectricTestRunner.class)
63 public class BatteryInfoTest {
64 
65     private static final String STATUS_CHARGING_NO_TIME = "50% - charging";
66     private static final String STATUS_CHARGING_TIME = "50% - 0 min left until full";
67     private static final String STATUS_NOT_CHARGING = "Not charging";
68     private static final long REMAINING_TIME_NULL = -1;
69     private static final long REMAINING_TIME = 2;
70     // Strings are defined in frameworks/base/packages/SettingsLib/res/values/strings.xml
71     private static final String ENHANCED_STRING_SUFFIX = "based on your usage";
72     private static final String BATTERY_RUN_OUT_PREFIX = "Battery may run out by";
73     private static final long TEST_CHARGE_TIME_REMAINING = TimeUnit.MINUTES.toMicros(1);
74     private static final String TEST_CHARGE_TIME_REMAINING_STRINGIFIED =
75             "1 min left until full";
76     private static final String TEST_BATTERY_LEVEL_10 = "10%";
77     private static final String FIFTEEN_MIN_FORMATTED = "15 min";
78     private static final Estimate MOCK_ESTIMATE = new Estimate(
79             1000, /* estimateMillis */
80             false, /* isBasedOnUsage */
81             1000 /* averageDischargeTime */);
82 
83     private Intent mDisChargingBatteryBroadcast;
84     private Intent mChargingBatteryBroadcast;
85     private Context mContext;
86     private FakeFeatureFactory mFeatureFactory;
87     @Mock
88     private BatteryUsageStats mBatteryUsageStats;
89 
90     @Before
setUp()91     public void setUp() {
92         MockitoAnnotations.initMocks(this);
93         mContext = spy(RuntimeEnvironment.application);
94         mFeatureFactory = FakeFeatureFactory.setupForTest();
95 
96         mDisChargingBatteryBroadcast = BatteryTestUtils.getDischargingIntent();
97 
98         mChargingBatteryBroadcast = BatteryTestUtils.getChargingIntent();
99     }
100 
101     @Test
testGetBatteryInfo_hasStatusLabel()102     public void testGetBatteryInfo_hasStatusLabel() {
103         doReturn(REMAINING_TIME_NULL).when(mBatteryUsageStats).getBatteryTimeRemainingMs();
104         BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext,
105                 mDisChargingBatteryBroadcast, mBatteryUsageStats,
106                 SystemClock.elapsedRealtime() * 1000,
107                 true /* shortString */);
108 
109         assertThat(info.statusLabel).isEqualTo(STATUS_NOT_CHARGING);
110     }
111 
112     @Test
testGetBatteryInfo_doNotShowChargingMethod_hasRemainingTime()113     public void testGetBatteryInfo_doNotShowChargingMethod_hasRemainingTime() {
114         doReturn(REMAINING_TIME).when(mBatteryUsageStats).getChargeTimeRemainingMs();
115         BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast,
116                 mBatteryUsageStats, SystemClock.elapsedRealtime() * 1000, false /* shortString */);
117 
118         assertThat(info.chargeLabel.toString()).isEqualTo(STATUS_CHARGING_TIME);
119     }
120 
121     @Test
testGetBatteryInfo_doNotShowChargingMethod_noRemainingTime()122     public void testGetBatteryInfo_doNotShowChargingMethod_noRemainingTime() {
123         doReturn(REMAINING_TIME_NULL).when(mBatteryUsageStats).getChargeTimeRemainingMs();
124         BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast,
125                 mBatteryUsageStats, SystemClock.elapsedRealtime() * 1000, false /* shortString */);
126 
127         assertThat(info.chargeLabel.toString()).isEqualTo(STATUS_CHARGING_NO_TIME);
128     }
129 
130     @Test
testGetBatteryInfo_pluggedInUsingShortString_usesCorrectData()131     public void testGetBatteryInfo_pluggedInUsingShortString_usesCorrectData() {
132         doReturn(TEST_CHARGE_TIME_REMAINING / 1000)
133                 .when(mBatteryUsageStats).getChargeTimeRemainingMs();
134         BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast,
135                 mBatteryUsageStats, SystemClock.elapsedRealtime() * 1000, true /* shortString */);
136 
137         assertThat(info.discharging).isEqualTo(false);
138         assertThat(info.chargeLabel.toString()).isEqualTo("50% - 1 min left until full");
139     }
140 
141     @Test
testGetBatteryInfo_basedOnUsageTrueMoreThanFifteenMinutes_usesCorrectString()142     public void testGetBatteryInfo_basedOnUsageTrueMoreThanFifteenMinutes_usesCorrectString() {
143         Estimate estimate = new Estimate(Duration.ofHours(4).toMillis(),
144                 true /* isBasedOnUsage */,
145                 1000 /* averageDischargeTime */);
146         BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
147                 mBatteryUsageStats, estimate, SystemClock.elapsedRealtime() * 1000,
148                 false /* shortString */);
149         BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
150                 mBatteryUsageStats, estimate, SystemClock.elapsedRealtime() * 1000,
151                 true /* shortString */);
152 
153         // Both long and short strings should not have extra text
154         assertThat(info.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
155         assertThat(info.suggestionLabel).contains(BATTERY_RUN_OUT_PREFIX);
156         assertThat(info2.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
157         assertThat(info2.suggestionLabel).contains(BATTERY_RUN_OUT_PREFIX);
158     }
159 
160     @Test
testGetBatteryInfo_basedOnUsageTrueLessThanSevenMinutes_usesCorrectString()161     public void testGetBatteryInfo_basedOnUsageTrueLessThanSevenMinutes_usesCorrectString() {
162         Estimate estimate = new Estimate(Duration.ofMinutes(7).toMillis(),
163                 true /* isBasedOnUsage */,
164                 1000 /* averageDischargeTime */);
165         BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
166                 mBatteryUsageStats, estimate, SystemClock.elapsedRealtime() * 1000,
167                 false /* shortString */);
168         BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
169                 mBatteryUsageStats, estimate, SystemClock.elapsedRealtime() * 1000,
170                 true /* shortString */);
171 
172         // These should be identical in either case
173         assertThat(info.remainingLabel.toString()).isEqualTo(
174                 mContext.getString(R.string.power_remaining_duration_only_shutdown_imminent));
175         assertThat(info2.remainingLabel.toString()).isEqualTo(
176                 mContext.getString(R.string.power_remaining_duration_only_shutdown_imminent));
177         assertThat(info2.suggestionLabel).contains(BATTERY_RUN_OUT_PREFIX);
178     }
179 
180     @Test
getBatteryInfo_MoreThanOneDay_suggestionLabelIsCorrectString()181     public void getBatteryInfo_MoreThanOneDay_suggestionLabelIsCorrectString() {
182         Estimate estimate = new Estimate(Duration.ofDays(3).toMillis(),
183                 true /* isBasedOnUsage */,
184                 1000 /* averageDischargeTime */);
185         BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
186                 mBatteryUsageStats, estimate, SystemClock.elapsedRealtime() * 1000,
187                 false /* shortString */);
188 
189         assertThat(info.suggestionLabel).doesNotContain(BATTERY_RUN_OUT_PREFIX);
190     }
191 
192     @Test
193     public void
testGetBatteryInfo_basedOnUsageTrueBetweenSevenAndFifteenMinutes_usesCorrectString()194     testGetBatteryInfo_basedOnUsageTrueBetweenSevenAndFifteenMinutes_usesCorrectString() {
195         Estimate estimate = new Estimate(Duration.ofMinutes(10).toMillis(),
196                 true /* isBasedOnUsage */,
197                 1000 /* averageDischargeTime */);
198         BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
199                 mBatteryUsageStats, estimate, SystemClock.elapsedRealtime() * 1000,
200                 false /* shortString */);
201 
202         // Check that strings are showing less than 15 minutes remaining regardless of exact time.
203         assertThat(info.chargeLabel.toString()).isEqualTo(
204                 mContext.getString(R.string.power_remaining_less_than_duration,
205                         FIFTEEN_MIN_FORMATTED, TEST_BATTERY_LEVEL_10));
206         assertThat(info.remainingLabel.toString()).isEqualTo(
207                 mContext.getString(R.string.power_remaining_less_than_duration_only,
208                         FIFTEEN_MIN_FORMATTED));
209     }
210 
211     @Test
testGetBatteryInfo_basedOnUsageFalse_usesDefaultString()212     public void testGetBatteryInfo_basedOnUsageFalse_usesDefaultString() {
213         BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
214                 mBatteryUsageStats, MOCK_ESTIMATE, SystemClock.elapsedRealtime() * 1000,
215                 false /* shortString */);
216         BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast,
217                 mBatteryUsageStats, MOCK_ESTIMATE, SystemClock.elapsedRealtime() * 1000,
218                 true /* shortString */);
219 
220         assertThat(info.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
221         assertThat(info2.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
222     }
223 
224     @Test
testGetBatteryInfo_charging_usesChargeTime()225     public void testGetBatteryInfo_charging_usesChargeTime() {
226         doReturn(TEST_CHARGE_TIME_REMAINING / 1000)
227                 .when(mBatteryUsageStats).getChargeTimeRemainingMs();
228 
229         BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mChargingBatteryBroadcast,
230                 mBatteryUsageStats, MOCK_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);
235     }
236 
237     @Test
testGetBatteryInfo_pluggedInWithFullBattery_onlyShowBatteryLevel()238     public void testGetBatteryInfo_pluggedInWithFullBattery_onlyShowBatteryLevel() {
239         mChargingBatteryBroadcast.putExtra(BatteryManager.EXTRA_LEVEL, 100);
240 
241         BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mChargingBatteryBroadcast,
242                 mBatteryUsageStats, MOCK_ESTIMATE, SystemClock.elapsedRealtime() * 1000,
243                 false /* shortString */);
244 
245         assertThat(info.chargeLabel).isEqualTo("100%");
246     }
247 
248     @Test
testGetBatteryInfo_chargingWithOverheated_updateChargeLabel()249     public void testGetBatteryInfo_chargingWithOverheated_updateChargeLabel() {
250         doReturn(TEST_CHARGE_TIME_REMAINING)
251                 .when(mBatteryUsageStats)
252                 .getChargeTimeRemainingMs();
253         mChargingBatteryBroadcast
254                 .putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT);
255 
256         BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mChargingBatteryBroadcast,
257                 mBatteryUsageStats, MOCK_ESTIMATE, SystemClock.elapsedRealtime() * 1000,
258                 false /* shortString */);
259 
260         assertThat(info.isOverheated).isTrue();
261         assertThat(info.chargeLabel).isEqualTo("50% - Charging temporarily limited");
262     }
263 
264     // Make our battery stats return a sequence of battery events.
mockBatteryStatsHistory()265     private void mockBatteryStatsHistory() {
266         // Mock out new data every time iterateBatteryStatsHistory is called.
267         doAnswer(invocation -> {
268             BatteryStatsHistoryIterator iterator = mock(BatteryStatsHistoryIterator.class);
269             doAnswer(new Answer<Boolean>() {
270                 private int mCount = 0;
271                 private final long[] mTimes = {1000, 1500, 2000};
272                 private final byte[] mLevels = {99, 98, 97};
273 
274                 @Override
275                 public Boolean answer(InvocationOnMock invocation) throws Throwable {
276                     if (mCount == mTimes.length) {
277                         return false;
278                     }
279                     BatteryStats.HistoryItem record = invocation.getArgument(0);
280                     record.cmd = BatteryStats.HistoryItem.CMD_UPDATE;
281                     record.time = mTimes[mCount];
282                     record.batteryLevel = mLevels[mCount];
283                     mCount++;
284                     return true;
285                 }
286             }).when(iterator).next(any(BatteryStats.HistoryItem.class));
287             return iterator;
288         }).when(mBatteryUsageStats).iterateBatteryStatsHistory();
289     }
290 
assertOnlyHistory(BatteryInfo info)291     private void assertOnlyHistory(BatteryInfo info) {
292         mockBatteryStatsHistory();
293         UsageView view = mock(UsageView.class);
294         when(view.getContext()).thenReturn(mContext);
295 
296         info.bindHistory(view);
297         verify(view, times(1)).configureGraph(anyInt(), anyInt());
298         verify(view, times(1)).addPath(any(SparseIntArray.class));
299         verify(view, never()).addProjectedPath(any(SparseIntArray.class));
300     }
301 
assertHistoryAndLinearProjection(BatteryInfo info)302     private void assertHistoryAndLinearProjection(BatteryInfo info) {
303         mockBatteryStatsHistory();
304         UsageView view = mock(UsageView.class);
305         when(view.getContext()).thenReturn(mContext);
306 
307         info.bindHistory(view);
308         verify(view, times(2)).configureGraph(anyInt(), anyInt());
309         verify(view, times(1)).addPath(any(SparseIntArray.class));
310         ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class);
311         verify(view, times(1)).addProjectedPath(pointsActual.capture());
312 
313         // Check that we have two points and the first is correct.
314         assertThat(pointsActual.getValue().size()).isEqualTo(2);
315         assertThat(pointsActual.getValue().keyAt(0)).isEqualTo(2000);
316         assertThat(pointsActual.getValue().valueAt(0)).isEqualTo(97);
317     }
318 
assertHistoryAndEnhancedProjection(BatteryInfo info)319     private void assertHistoryAndEnhancedProjection(BatteryInfo info) {
320         mockBatteryStatsHistory();
321         UsageView view = mock(UsageView.class);
322         when(view.getContext()).thenReturn(mContext);
323         SparseIntArray pointsExpected = new SparseIntArray();
324         pointsExpected.append(2000, 96);
325         pointsExpected.append(2500, 95);
326         pointsExpected.append(3000, 94);
327         doReturn(pointsExpected).when(mFeatureFactory.powerUsageFeatureProvider)
328                 .getEnhancedBatteryPredictionCurve(any(Context.class), anyLong());
329 
330         info.bindHistory(view);
331         verify(view, times(2)).configureGraph(anyInt(), anyInt());
332         verify(view, times(1)).addPath(any(SparseIntArray.class));
333         ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class);
334         verify(view, times(1)).addProjectedPath(pointsActual.capture());
335         assertThat(pointsActual.getValue()).isEqualTo(pointsExpected);
336     }
337 
getBatteryInfo(boolean charging, boolean enhanced, boolean estimate)338     private BatteryInfo getBatteryInfo(boolean charging, boolean enhanced, boolean estimate) {
339         if (charging && estimate) {
340             doReturn(1000L).when(mBatteryUsageStats).getChargeTimeRemainingMs();
341         } else {
342             doReturn(0L).when(mBatteryUsageStats).getChargeTimeRemainingMs();
343         }
344         Estimate batteryEstimate = new Estimate(
345                 estimate ? 1000 : 0,
346                 false /* isBasedOnUsage */,
347                 1000 /* averageDischargeTime */);
348         BatteryInfo info = BatteryInfo.getBatteryInfo(mContext,
349                 charging ? mChargingBatteryBroadcast : mDisChargingBatteryBroadcast,
350                 mBatteryUsageStats, batteryEstimate, SystemClock.elapsedRealtime() * 1000, false);
351         doReturn(enhanced).when(mFeatureFactory.powerUsageFeatureProvider)
352                 .isEnhancedBatteryPredictionEnabled(mContext);
353         return info;
354     }
355 
356     @Test
testBindHistory()357     public void testBindHistory() {
358         BatteryInfo info;
359 
360         info = getBatteryInfo(false /* charging */, false /* enhanced */, false /* estimate */);
361         assertOnlyHistory(info);
362 
363         info = getBatteryInfo(false /* charging */, false /* enhanced */, true /* estimate */);
364         assertHistoryAndLinearProjection(info);
365 
366         info = getBatteryInfo(false /* charging */, true /* enhanced */, false /* estimate */);
367         assertOnlyHistory(info);
368 
369         info = getBatteryInfo(false /* charging */, true /* enhanced */, true /* estimate */);
370         assertHistoryAndEnhancedProjection(info);
371 
372         info = getBatteryInfo(true /* charging */, false /* enhanced */, false /* estimate */);
373         assertOnlyHistory(info);
374 
375         info = getBatteryInfo(true /* charging */, false /* enhanced */, true /* estimate */);
376         assertHistoryAndLinearProjection(info);
377 
378         info = getBatteryInfo(true /* charging */, true /* enhanced */, false /* estimate */);
379         assertOnlyHistory(info);
380 
381         // Linear projection for charging even in enhanced mode.
382         info = getBatteryInfo(true /* charging */, true /* enhanced */, true /* estimate */);
383         assertHistoryAndLinearProjection(info);
384     }
385 }
386