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 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import static org.mockito.ArgumentMatchers.anyInt;
23 import static org.mockito.ArgumentMatchers.anyLong;
24 import static org.mockito.ArgumentMatchers.anyString;
25 import static org.mockito.ArgumentMatchers.eq;
26 import static org.mockito.Mockito.any;
27 import static org.mockito.Mockito.doAnswer;
28 import static org.mockito.Mockito.doReturn;
29 import static org.mockito.Mockito.mock;
30 import static org.mockito.Mockito.never;
31 import static org.mockito.Mockito.spy;
32 import static org.mockito.Mockito.times;
33 import static org.mockito.Mockito.verify;
34 import static org.mockito.Mockito.when;
35 
36 import android.content.Context;
37 import android.content.Intent;
38 import android.os.BatteryManager;
39 import android.os.BatteryStats;
40 import android.os.BatteryUsageStats;
41 import android.os.SystemClock;
42 import android.os.SystemProperties;
43 import android.provider.Settings;
44 import android.util.SparseIntArray;
45 
46 import com.android.internal.os.BatteryStatsHistoryIterator;
47 import com.android.settings.testutils.BatteryTestUtils;
48 import com.android.settings.testutils.FakeFeatureFactory;
49 import com.android.settings.widget.UsageView;
50 import com.android.settingslib.fuelgauge.Estimate;
51 
52 import org.junit.After;
53 import org.junit.Before;
54 import org.junit.Ignore;
55 import org.junit.Test;
56 import org.junit.runner.RunWith;
57 import org.mockito.ArgumentCaptor;
58 import org.mockito.Mock;
59 import org.mockito.MockitoAnnotations;
60 import org.robolectric.RobolectricTestRunner;
61 import org.robolectric.RuntimeEnvironment;
62 
63 import java.time.Duration;
64 import java.time.Instant;
65 import java.util.Locale;
66 import java.util.Map;
67 import java.util.TimeZone;
68 import java.util.concurrent.TimeUnit;
69 
70 @RunWith(RobolectricTestRunner.class)
71 public class BatteryInfoTest {
72 
73     private static final String STATUS_CHARGING_NO_TIME = "50% - charging";
74     private static final String STATUS_CHARGING_TIME = "50% - 0 min left until full";
75     private static final String STATUS_NOT_CHARGING = "Not charging";
76     private static final String STATUS_CHARGING_FUTURE_BYPASS = "50% - Charging";
77     private static final String STATUS_CHARGING_PAUSED = "50% - Charging optimized";
78     private static final long REMAINING_TIME_NULL = -1;
79     private static final long REMAINING_TIME = 2;
80     // Strings are defined in frameworks/base/packages/SettingsLib/res/values/strings.xml
81     private static final String ENHANCED_STRING_SUFFIX = "based on your usage";
82     private static final String BATTERY_RUN_OUT_PREFIX = "Battery may run out by";
83     private static final long TEST_CHARGE_TIME_REMAINING = TimeUnit.MINUTES.toMicros(1);
84     private static final String TEST_CHARGE_TIME_REMAINING_STRINGIFIED = "1 min left until full";
85     private static final String TEST_BATTERY_LEVEL_10 = "10%";
86     private static final String FIFTEEN_MIN_FORMATTED = "15 min";
87     private static final Estimate MOCK_ESTIMATE =
88             new Estimate(
89                     1000, /* estimateMillis */
90                     false, /* isBasedOnUsage */
91                     1000 /* averageDischargeTime */);
92     private static final Map<ChargingType, Integer> CHARGING_TYPE_MAP =
93             Map.of(
94                     ChargingType.WIRED, BatteryManager.BATTERY_PLUGGED_AC,
95                     ChargingType.WIRELESS, BatteryManager.BATTERY_PLUGGED_WIRELESS,
96                     ChargingType.DOCKED, BatteryManager.BATTERY_PLUGGED_DOCK);
97     private static final Map<ChargingSpeed, Integer> CHARGING_SPEED_MAP =
98             Map.of(
99                     ChargingSpeed.FAST, 1501000,
100                     ChargingSpeed.REGULAR, 1500000,
101                     ChargingSpeed.SLOW, 999999);
102     private static final long UNUSED_TIME_MS = -1L;
103 
104     private Intent mDisChargingBatteryBroadcast;
105     private Intent mChargingBatteryBroadcast;
106     private Context mContext;
107     private FakeFeatureFactory mFeatureFactory;
108     private TimeZone mOriginalTimeZone;
109 
110     @Mock private BatteryUsageStats mBatteryUsageStats;
111 
112     @Before
setUp()113     public void setUp() {
114         MockitoAnnotations.initMocks(this);
115         mContext = spy(RuntimeEnvironment.application);
116         mFeatureFactory = FakeFeatureFactory.setupForTest();
117 
118         mDisChargingBatteryBroadcast = BatteryTestUtils.getDischargingIntent();
119 
120         mChargingBatteryBroadcast = BatteryTestUtils.getChargingIntent();
121 
122         doReturn(false).when(mFeatureFactory.powerUsageFeatureProvider).isExtraDefend();
123         Settings.Global.putInt(
124                 mContext.getContentResolver(),
125                 BatteryUtils.SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS,
126                 0);
127 
128         // Reset static cache for testing purpose.
129         com.android.settingslib.fuelgauge.BatteryUtils.setChargingStringV2Enabled(null);
130 
131         mOriginalTimeZone = TimeZone.getDefault();
132     }
133 
134     @After
tearDown()135     public void tearDown() throws Exception {
136         TimeZone.setDefault(mOriginalTimeZone);
137     }
138 
139     @Test
getBatteryInfo_hasStatusLabel()140     public void getBatteryInfo_hasStatusLabel() {
141         doReturn(REMAINING_TIME_NULL).when(mBatteryUsageStats).getBatteryTimeRemainingMs();
142         BatteryInfo info =
143                 BatteryInfo.getBatteryInfoOld(
144                         mContext,
145                         mDisChargingBatteryBroadcast,
146                         mBatteryUsageStats,
147                         SystemClock.elapsedRealtime() * 1000,
148                         true /* shortString */);
149 
150         assertThat(info.statusLabel).isEqualTo(STATUS_NOT_CHARGING);
151     }
152 
153     @Test
getBatteryInfo_doNotShowChargingMethod_hasRemainingTime()154     public void getBatteryInfo_doNotShowChargingMethod_hasRemainingTime() {
155         doReturn(REMAINING_TIME).when(mBatteryUsageStats).getChargeTimeRemainingMs();
156         BatteryInfo info =
157                 BatteryInfo.getBatteryInfoOld(
158                         mContext,
159                         mChargingBatteryBroadcast,
160                         mBatteryUsageStats,
161                         SystemClock.elapsedRealtime() * 1000,
162                         false /* shortString */);
163 
164         assertThat(info.chargeLabel.toString()).isEqualTo(STATUS_CHARGING_TIME);
165     }
166 
167     @Test
getBatteryInfo_doNotShowChargingMethod_noRemainingTime()168     public void getBatteryInfo_doNotShowChargingMethod_noRemainingTime() {
169         doReturn(REMAINING_TIME_NULL).when(mBatteryUsageStats).getChargeTimeRemainingMs();
170         BatteryInfo info =
171                 BatteryInfo.getBatteryInfoOld(
172                         mContext,
173                         mChargingBatteryBroadcast,
174                         mBatteryUsageStats,
175                         SystemClock.elapsedRealtime() * 1000,
176                         false /* shortString */);
177 
178         assertThat(info.chargeLabel.toString()).ignoringCase().isEqualTo(STATUS_CHARGING_NO_TIME);
179     }
180 
181     @Test
getBatteryInfo_pluggedInUsingShortString_usesCorrectData()182     public void getBatteryInfo_pluggedInUsingShortString_usesCorrectData() {
183         doReturn(TEST_CHARGE_TIME_REMAINING / 1000)
184                 .when(mBatteryUsageStats)
185                 .getChargeTimeRemainingMs();
186         BatteryInfo info =
187                 BatteryInfo.getBatteryInfoOld(
188                         mContext,
189                         mChargingBatteryBroadcast,
190                         mBatteryUsageStats,
191                         SystemClock.elapsedRealtime() * 1000,
192                         true /* shortString */);
193 
194         assertThat(info.discharging).isEqualTo(false);
195         assertThat(info.chargeLabel.toString()).isEqualTo("50% - 1 min left until full");
196     }
197 
198     @Test
getBatteryInfo_basedOnUsageTrueMoreThanFifteenMinutes_usesCorrectString()199     public void getBatteryInfo_basedOnUsageTrueMoreThanFifteenMinutes_usesCorrectString() {
200         Estimate estimate =
201                 new Estimate(
202                         Duration.ofHours(4).toMillis(),
203                         true /* isBasedOnUsage */,
204                         1000 /* averageDischargeTime */);
205         BatteryInfo info =
206                 BatteryInfo.getBatteryInfo(
207                         mContext,
208                         mDisChargingBatteryBroadcast,
209                         mBatteryUsageStats,
210                         estimate,
211                         SystemClock.elapsedRealtime() * 1000,
212                         false /* shortString */);
213         BatteryInfo info2 =
214                 BatteryInfo.getBatteryInfo(
215                         mContext,
216                         mDisChargingBatteryBroadcast,
217                         mBatteryUsageStats,
218                         estimate,
219                         SystemClock.elapsedRealtime() * 1000,
220                         true /* shortString */);
221 
222         // Both long and short strings should not have extra text
223         assertThat(info.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
224         assertThat(info.suggestionLabel).contains(BATTERY_RUN_OUT_PREFIX);
225         assertThat(info2.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
226         assertThat(info2.suggestionLabel).contains(BATTERY_RUN_OUT_PREFIX);
227     }
228 
229     @Test
230     @Ignore
getBatteryInfo_MoreThanOneDay_suggestionLabelIsCorrectString()231     public void getBatteryInfo_MoreThanOneDay_suggestionLabelIsCorrectString() {
232         Estimate estimate =
233                 new Estimate(
234                         Duration.ofDays(3).toMillis(),
235                         true /* isBasedOnUsage */,
236                         1000 /* averageDischargeTime */);
237         BatteryInfo info =
238                 BatteryInfo.getBatteryInfo(
239                         mContext,
240                         mDisChargingBatteryBroadcast,
241                         mBatteryUsageStats,
242                         estimate,
243                         SystemClock.elapsedRealtime() * 1000,
244                         false /* shortString */);
245 
246         assertThat(info.suggestionLabel).doesNotContain(BATTERY_RUN_OUT_PREFIX);
247     }
248 
249     @Test
getBatteryInfo_basedOnUsageFalse_usesDefaultString()250     public void getBatteryInfo_basedOnUsageFalse_usesDefaultString() {
251         BatteryInfo info =
252                 BatteryInfo.getBatteryInfo(
253                         mContext,
254                         mDisChargingBatteryBroadcast,
255                         mBatteryUsageStats,
256                         MOCK_ESTIMATE,
257                         SystemClock.elapsedRealtime() * 1000,
258                         false /* shortString */);
259         BatteryInfo info2 =
260                 BatteryInfo.getBatteryInfo(
261                         mContext,
262                         mDisChargingBatteryBroadcast,
263                         mBatteryUsageStats,
264                         MOCK_ESTIMATE,
265                         SystemClock.elapsedRealtime() * 1000,
266                         true /* shortString */);
267 
268         assertThat(info.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
269         assertThat(info2.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX);
270     }
271 
272     @Test
getBatteryInfo_charging_usesChargeTime()273     public void getBatteryInfo_charging_usesChargeTime() {
274         doReturn(TEST_CHARGE_TIME_REMAINING / 1000)
275                 .when(mBatteryUsageStats)
276                 .getChargeTimeRemainingMs();
277 
278         BatteryInfo info =
279                 BatteryInfo.getBatteryInfo(
280                         mContext,
281                         mChargingBatteryBroadcast,
282                         mBatteryUsageStats,
283                         MOCK_ESTIMATE,
284                         SystemClock.elapsedRealtime() * 1000,
285                         false /* shortString */);
286 
287         assertThat(info.remainingTimeUs).isEqualTo(TEST_CHARGE_TIME_REMAINING);
288         assertThat(info.remainingLabel.toString())
289                 .isEqualTo(TEST_CHARGE_TIME_REMAINING_STRINGIFIED);
290     }
291 
292     @Test
getBatteryInfo_pluggedInWithFullBattery_onlyShowBatteryLevel()293     public void getBatteryInfo_pluggedInWithFullBattery_onlyShowBatteryLevel() {
294         mChargingBatteryBroadcast.putExtra(BatteryManager.EXTRA_LEVEL, 100);
295 
296         BatteryInfo info =
297                 BatteryInfo.getBatteryInfo(
298                         mContext,
299                         mChargingBatteryBroadcast,
300                         mBatteryUsageStats,
301                         MOCK_ESTIMATE,
302                         SystemClock.elapsedRealtime() * 1000,
303                         false /* shortString */);
304 
305         assertThat(info.chargeLabel).isEqualTo("100%");
306     }
307 
308     @Test
getBatteryInfo_chargingWithDefender_updateChargeLabel()309     public void getBatteryInfo_chargingWithDefender_updateChargeLabel() {
310         doReturn(TEST_CHARGE_TIME_REMAINING).when(mBatteryUsageStats).getChargeTimeRemainingMs();
311         doReturn(true)
312                 .when(mFeatureFactory.powerUsageFeatureProvider)
313                 .isBatteryDefend(any(BatteryInfo.class));
314         mChargingBatteryBroadcast.putExtra(
315                 BatteryManager.EXTRA_CHARGING_STATUS,
316                 BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE);
317 
318         BatteryInfo info =
319                 BatteryInfo.getBatteryInfo(
320                         mContext,
321                         mChargingBatteryBroadcast,
322                         mBatteryUsageStats,
323                         MOCK_ESTIMATE,
324                         SystemClock.elapsedRealtime() * 1000,
325                         false /* shortString */);
326 
327         assertThat(info.isBatteryDefender).isTrue();
328         assertThat(info.chargeLabel.toString()).contains(STATUS_CHARGING_PAUSED);
329     }
330 
331     @Test
getBatteryInfo_getChargeTimeRemaining_updateSettingsGlobal()332     public void getBatteryInfo_getChargeTimeRemaining_updateSettingsGlobal() {
333         doReturn(TEST_CHARGE_TIME_REMAINING).when(mBatteryUsageStats).getChargeTimeRemainingMs();
334 
335         BatteryInfo.getBatteryInfo(
336                 mContext,
337                 mChargingBatteryBroadcast,
338                 mBatteryUsageStats,
339                 MOCK_ESTIMATE,
340                 SystemClock.elapsedRealtime() * 1000,
341                 false /* shortString */);
342 
343         assertThat(BatteryInfo.getSettingsChargeTimeRemaining(mContext))
344                 .isEqualTo(TEST_CHARGE_TIME_REMAINING);
345     }
346 
347     @Test
getBatteryInfo_differentChargeTimeRemaining_updateSettingsGlobal()348     public void getBatteryInfo_differentChargeTimeRemaining_updateSettingsGlobal() {
349         doReturn(TEST_CHARGE_TIME_REMAINING).when(mBatteryUsageStats).getChargeTimeRemainingMs();
350         final long newTimeToFull = 300L;
351         doReturn(newTimeToFull).when(mBatteryUsageStats).getChargeTimeRemainingMs();
352 
353         BatteryInfo.getBatteryInfo(
354                 mContext,
355                 mChargingBatteryBroadcast,
356                 mBatteryUsageStats,
357                 MOCK_ESTIMATE,
358                 SystemClock.elapsedRealtime() * 1000,
359                 false /* shortString */);
360 
361         assertThat(BatteryInfo.getSettingsChargeTimeRemaining(mContext)).isEqualTo(newTimeToFull);
362     }
363 
364     @Test
getBatteryInfo_dockDefenderActive_updateChargeString()365     public void getBatteryInfo_dockDefenderActive_updateChargeString() {
366         doReturn(TEST_CHARGE_TIME_REMAINING / 1000)
367                 .when(mBatteryUsageStats)
368                 .getChargeTimeRemainingMs();
369         doReturn(true).when(mFeatureFactory.powerUsageFeatureProvider).isExtraDefend();
370         doReturn(true)
371                 .when(mFeatureFactory.powerUsageFeatureProvider)
372                 .isBatteryDefend(any(BatteryInfo.class));
373         Intent intent =
374                 createBatteryIntent(
375                                 BatteryManager.BATTERY_PLUGGED_DOCK,
376                                 /* level= */ 50,
377                                 BatteryManager.BATTERY_STATUS_CHARGING)
378                         .putExtra(
379                                 BatteryManager.EXTRA_CHARGING_STATUS,
380                                 BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE);
381 
382         BatteryInfo info =
383                 BatteryInfo.getBatteryInfo(
384                         mContext,
385                         intent,
386                         mBatteryUsageStats,
387                         MOCK_ESTIMATE,
388                         SystemClock.elapsedRealtime() * 1000,
389                         false /* shortString */);
390 
391         assertThat(info.chargeLabel.toString()).contains(STATUS_CHARGING_PAUSED);
392     }
393 
394     @Test
getBatteryInfo_dockDefenderTemporarilyBypassed_updateChargeLabel()395     public void getBatteryInfo_dockDefenderTemporarilyBypassed_updateChargeLabel() {
396         doReturn(REMAINING_TIME).when(mBatteryUsageStats).getChargeTimeRemainingMs();
397         mChargingBatteryBroadcast.putExtra(
398                 BatteryManager.EXTRA_CHARGING_STATUS, BatteryManager.CHARGING_POLICY_DEFAULT);
399         Settings.Global.putInt(
400                 mContext.getContentResolver(),
401                 BatteryUtils.SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS,
402                 1);
403 
404         BatteryInfo info =
405                 BatteryInfo.getBatteryInfo(
406                         mContext,
407                         createBatteryIntent(
408                                 BatteryManager.BATTERY_PLUGGED_DOCK,
409                                 /* level= */ 50,
410                                 BatteryManager.BATTERY_STATUS_CHARGING),
411                         mBatteryUsageStats,
412                         MOCK_ESTIMATE,
413                         SystemClock.elapsedRealtime() * 1000,
414                         false /* shortString */);
415 
416         assertThat(info.chargeLabel.toString()).contains(STATUS_CHARGING_TIME);
417     }
418 
419     @Test
getBatteryInfo_dockDefenderFutureBypass_updateChargeLabel()420     public void getBatteryInfo_dockDefenderFutureBypass_updateChargeLabel() {
421         doReturn(false).when(mFeatureFactory.powerUsageFeatureProvider).isExtraDefend();
422         mChargingBatteryBroadcast.putExtra(
423                 BatteryManager.EXTRA_CHARGING_STATUS, BatteryManager.CHARGING_POLICY_DEFAULT);
424 
425         BatteryInfo info =
426                 BatteryInfo.getBatteryInfo(
427                         mContext,
428                         createBatteryIntent(
429                                 BatteryManager.BATTERY_PLUGGED_DOCK,
430                                 /* level= */ 50,
431                                 BatteryManager.BATTERY_STATUS_CHARGING),
432                         mBatteryUsageStats,
433                         MOCK_ESTIMATE,
434                         SystemClock.elapsedRealtime() * 1000,
435                         false /* shortString */);
436 
437         assertThat(info.chargeLabel.toString()).contains(STATUS_CHARGING_FUTURE_BYPASS);
438     }
439 
440     @Test
getBatteryInfo_fastCharging_updateRemainingLabelAndStatusLabel()441     public void getBatteryInfo_fastCharging_updateRemainingLabelAndStatusLabel() {
442         prepareTestGetBatteryInfoEnvironment(
443                 /* remainingTimeMs= */ Duration.ofMinutes(90).toMillis(),
444                 /* chargingStringV2Enabled= */ false);
445         Intent batteryIntent =
446                 createIntentForGetBatteryInfoTest(
447                         ChargingType.WIRED, ChargingSpeed.FAST, /* batteryLevel= */ 61);
448         var expectedStatusLabel = "Charging rapidly";
449         var expectedRemainingLabel = "1 hr, 30 min left until full";
450         var expectedChargeLabel = "61% - " + expectedRemainingLabel;
451 
452         assertGetBatteryInfo(
453                 batteryIntent,
454                 /* currentTimeMillis= */ UNUSED_TIME_MS,
455                 expectedStatusLabel,
456                 expectedRemainingLabel,
457                 expectedChargeLabel);
458     }
459 
460     @Test
getBatteryInfo_regularCharging_updateRemainingLabelAndStatusLabel()461     public void getBatteryInfo_regularCharging_updateRemainingLabelAndStatusLabel() {
462         prepareTestGetBatteryInfoEnvironment(
463                 /* remainingTimeMs= */ Duration.ofMinutes(80).toMillis(),
464                 /* chargingStringV2Enabled= */ false);
465         Intent batteryIntent =
466                 createIntentForGetBatteryInfoTest(
467                         ChargingType.WIRED, ChargingSpeed.REGULAR, /* batteryLevel= */ 33);
468         var expectedStatusLabel = "Charging";
469         var expectedRemainingLabel = "1 hr, 20 min left until full";
470         var expectedChargeLabel = "33% - " + expectedRemainingLabel;
471 
472         assertGetBatteryInfo(
473                 batteryIntent,
474                 /* currentTimeMillis= */ UNUSED_TIME_MS,
475                 expectedStatusLabel,
476                 expectedRemainingLabel,
477                 expectedChargeLabel);
478     }
479 
480     @Test
getBatteryInfo_slowCharging_updateRemainingLabelAndStatusLabel()481     public void getBatteryInfo_slowCharging_updateRemainingLabelAndStatusLabel() {
482         prepareTestGetBatteryInfoEnvironment(
483                 /* remainingTimeMs= */ Duration.ofMinutes(100).toMillis(),
484                 /* chargingStringV2Enabled= */ false);
485         Intent batteryIntent =
486                 createIntentForGetBatteryInfoTest(
487                         ChargingType.WIRED, ChargingSpeed.SLOW, /* batteryLevel= */ 53);
488         var expectedStatusLabel = "Charging slowly";
489         var expectedRemainingLabel = "1 hr, 40 min left until full";
490         var expectedChargeLabel = "53% - " + expectedRemainingLabel;
491 
492         assertGetBatteryInfo(
493                 batteryIntent,
494                 /* currentTimeMillis= */ UNUSED_TIME_MS,
495                 expectedStatusLabel,
496                 expectedRemainingLabel,
497                 expectedChargeLabel);
498     }
499 
500     @Test
getBatteryInfo_wirelessCharging_updateRemainingLabelAndStatusLabel()501     public void getBatteryInfo_wirelessCharging_updateRemainingLabelAndStatusLabel() {
502         prepareTestGetBatteryInfoEnvironment(
503                 /* remainingTimeMs= */ Duration.ofMinutes(130).toMillis(),
504                 /* chargingStringV2Enabled= */ false);
505         Intent batteryIntent =
506                 createIntentForGetBatteryInfoTest(
507                         ChargingType.WIRELESS, ChargingSpeed.REGULAR, /* batteryLevel= */ 10);
508         var expectedStatusLabel = "Charging wirelessly";
509         var expectedRemainingLabel = "2 hr, 10 min left until full";
510         var expectedChargeLabel = "10% - " + expectedRemainingLabel;
511 
512         assertGetBatteryInfo(
513                 batteryIntent,
514                 /* currentTimeMillis= */ UNUSED_TIME_MS,
515                 expectedStatusLabel,
516                 expectedRemainingLabel,
517                 expectedChargeLabel);
518     }
519 
520     @Test
getBatteryInfo_dockedCharging_updateRemainingLabelAndStatusLabel()521     public void getBatteryInfo_dockedCharging_updateRemainingLabelAndStatusLabel() {
522         prepareTestGetBatteryInfoEnvironment(
523                 /* remainingTimeMs= */ Duration.ofMinutes(30).toMillis(),
524                 /* chargingStringV2Enabled= */ false);
525         Intent batteryIntent =
526                 createIntentForGetBatteryInfoTest(
527                         ChargingType.DOCKED, ChargingSpeed.REGULAR, /* batteryLevel= */ 51);
528         var expectedStatusLabel = "Charging";
529         var expectedRemainingLabel = "30 min left until full";
530         var expectedChargeLabel = "51% - " + expectedRemainingLabel;
531 
532         assertGetBatteryInfo(
533                 batteryIntent,
534                 /* currentTimeMillis= */ UNUSED_TIME_MS,
535                 expectedStatusLabel,
536                 expectedRemainingLabel,
537                 expectedChargeLabel);
538     }
539 
540     @Test
541     @Ignore
getBatteryInfo_fastChargingV2_updateRemainingLabelAndStatusLabel()542     public void getBatteryInfo_fastChargingV2_updateRemainingLabelAndStatusLabel() {
543         prepareTestGetBatteryInfoEnvironment(
544                 /* remainingTimeMs= */ Duration.ofMinutes(30).toMillis(),
545                 /* chargingStringV2Enabled= */ true);
546         Intent batteryIntent =
547                 createIntentForGetBatteryInfoTest(
548                         ChargingType.WIRED, ChargingSpeed.FAST, /* batteryLevel= */ 56);
549         var expectedStatusLabel = "Fast charging";
550         var expectedRemainingLabel = "Full by ";
551         var expectedChargeLabel = "56% - " + expectedStatusLabel + " - " + expectedRemainingLabel;
552         var currentTimeMillis = Instant.parse("2024-04-01T13:00:00Z").toEpochMilli();
553 
554         assertGetBatteryInfo(
555                 batteryIntent,
556                 currentTimeMillis,
557                 expectedStatusLabel,
558                 expectedRemainingLabel,
559                 expectedChargeLabel);
560     }
561 
562     @Test
getBatteryInfo_regularChargingV2_updateRemainingLabelAndStatusLabel()563     public void getBatteryInfo_regularChargingV2_updateRemainingLabelAndStatusLabel() {
564         prepareTestGetBatteryInfoEnvironment(
565                 /* remainingTimeMs= */ Duration.ofHours(1).toMillis(),
566                 /* chargingStringV2Enabled= */ true);
567         Intent batteryIntent =
568                 createIntentForGetBatteryInfoTest(
569                         ChargingType.WIRED, ChargingSpeed.REGULAR, /* batteryLevel= */ 12);
570         var expectedStatusLabel = "Charging";
571         var expectedRemainingLabel = "Fully charged by ";
572         var expectedChargeLabel = "12% - " + expectedRemainingLabel;
573         var currentTimeMillis = Instant.parse("2024-04-01T13:00:00Z").toEpochMilli();
574 
575         assertGetBatteryInfo(
576                 batteryIntent,
577                 currentTimeMillis,
578                 expectedStatusLabel,
579                 expectedRemainingLabel,
580                 expectedChargeLabel);
581     }
582 
583     @Test
getBatteryInfo_slowChargingV2_updateRemainingLabelAndStatusLabel()584     public void getBatteryInfo_slowChargingV2_updateRemainingLabelAndStatusLabel() {
585         prepareTestGetBatteryInfoEnvironment(
586                 /* remainingTimeMs= */ Duration.ofHours(2).toMillis(),
587                 /* chargingStringV2Enabled= */ true);
588         Intent batteryIntent =
589                 createIntentForGetBatteryInfoTest(
590                         ChargingType.WIRED, ChargingSpeed.SLOW, /* batteryLevel= */ 18);
591         var expectedStatusLabel = "Charging";
592         var expectedRemainingLabel = "Fully charged by";
593         var expectedChargeLabel = "18% - " + expectedRemainingLabel;
594         var currentTimeMillis = Instant.parse("2024-04-01T13:00:00Z").toEpochMilli();
595 
596         assertGetBatteryInfo(
597                 batteryIntent,
598                 currentTimeMillis,
599                 expectedStatusLabel,
600                 expectedRemainingLabel,
601                 expectedChargeLabel);
602     }
603 
604     @Test
getBatteryInfo_wirelessChargingV2_updateRemainingLabelAndStatusLabel()605     public void getBatteryInfo_wirelessChargingV2_updateRemainingLabelAndStatusLabel() {
606         prepareTestGetBatteryInfoEnvironment(
607                 /* remainingTimeMs= */ Duration.ofHours(1).toMillis(),
608                 /* chargingStringV2Enabled= */ true);
609         Intent batteryIntent =
610                 createIntentForGetBatteryInfoTest(
611                         ChargingType.WIRELESS, ChargingSpeed.REGULAR, /* batteryLevel= */ 45);
612         var expectedStatusLabel = "Charging";
613         var expectedRemainingLabel = "Fully charged by";
614         var expectedChargeLabel = "45% - " + expectedRemainingLabel;
615         var currentTimeMillis = Instant.parse("2024-04-01T15:00:00Z").toEpochMilli();
616 
617         assertGetBatteryInfo(
618                 batteryIntent,
619                 currentTimeMillis,
620                 expectedStatusLabel,
621                 expectedRemainingLabel,
622                 expectedChargeLabel);
623     }
624 
625     @Test
getBatteryInfo_dockedChargingV2_updateRemainingLabelAndStatusLabel()626     public void getBatteryInfo_dockedChargingV2_updateRemainingLabelAndStatusLabel() {
627         prepareTestGetBatteryInfoEnvironment(
628                 /* remainingTimeMs= */ Duration.ofHours(1).toMillis(),
629                 /* chargingStringV2Enabled= */ true);
630         Intent batteryIntent =
631                 createIntentForGetBatteryInfoTest(
632                         ChargingType.DOCKED, ChargingSpeed.REGULAR, /* batteryLevel= */ 66);
633         var expectedStatusLabel = "Charging";
634         var expectedRemainingLabel = "Fully charged by";
635         var expectedChargeLabel = "66% - " + expectedRemainingLabel;
636         var currentTimeMillis = Instant.parse("2021-02-09T13:00:00.00Z").toEpochMilli();
637 
638         assertGetBatteryInfo(
639                 batteryIntent,
640                 currentTimeMillis,
641                 expectedStatusLabel,
642                 expectedRemainingLabel,
643                 expectedChargeLabel);
644     }
645 
646     @Test
getBatteryInfo_customizedWLCLabel_updateRemainingLabelAndStatusLabel()647     public void getBatteryInfo_customizedWLCLabel_updateRemainingLabelAndStatusLabel() {
648         prepareTestGetBatteryInfoEnvironment(
649                 /* remainingTimeMs= */ Duration.ofHours(1).toMillis(),
650                 /* chargingStringV2Enabled= */ true);
651         Intent batteryIntent =
652                 createIntentForGetBatteryInfoTest(
653                         ChargingType.WIRELESS, ChargingSpeed.REGULAR, /* batteryLevel= */ 45);
654         var expectedLabel = "Full by 8:00 AM";
655         when(mFeatureFactory.batterySettingsFeatureProvider.getWirelessChargingRemainingLabel(
656                         eq(mContext), anyLong(), anyLong()))
657                 .thenReturn(expectedLabel);
658         var currentTimeMillis = Instant.parse("2021-02-09T13:00:00.00Z").toEpochMilli();
659         var info =
660                 BatteryInfo.getBatteryInfo(
661                         mContext,
662                         batteryIntent,
663                         mBatteryUsageStats,
664                         MOCK_ESTIMATE,
665                         /* elapsedRealtimeUs= */ UNUSED_TIME_MS,
666                         /* shortString= */ false,
667                         /* currentTimeMillis= */ currentTimeMillis);
668 
669         assertThat(info.remainingLabel).isEqualTo(expectedLabel);
670     }
671 
672     @Test
getBatteryInfo_noCustomizedWLCLabel_updateRemainingLabelAndStatusLabel()673     public void getBatteryInfo_noCustomizedWLCLabel_updateRemainingLabelAndStatusLabel() {
674         prepareTestGetBatteryInfoEnvironment(
675                 /* remainingTimeMs= */ Duration.ofHours(1).toMillis(),
676                 /* chargingStringV2Enabled= */ true);
677         Intent batteryIntent =
678                 createIntentForGetBatteryInfoTest(
679                         ChargingType.WIRELESS, ChargingSpeed.REGULAR, /* batteryLevel= */ 45);
680         when(mFeatureFactory.batterySettingsFeatureProvider.getWirelessChargingRemainingLabel(
681                         eq(mContext), anyLong(), anyLong()))
682                 .thenReturn(null);
683         var expectedStatusLabel = "Charging";
684         var expectedRemainingLabel = "Fully charged by";
685         var expectedChargeLabel = "45% - " + expectedRemainingLabel;
686         var currentTimeMillis = Instant.parse("2024-04-01T15:00:00Z").toEpochMilli();
687 
688         assertGetBatteryInfo(
689                 batteryIntent,
690                 currentTimeMillis,
691                 expectedStatusLabel,
692                 expectedRemainingLabel,
693                 expectedChargeLabel);
694     }
695 
696     @Test
getBatteryInfo_noCustomWirelessChargingLabelWithV1_updateRemainingAndStatusLabel()697     public void getBatteryInfo_noCustomWirelessChargingLabelWithV1_updateRemainingAndStatusLabel() {
698         prepareTestGetBatteryInfoEnvironment(
699                 /* remainingTimeMs= */ Duration.ofMinutes(130).toMillis(),
700                 /* chargingStringV2Enabled= */ false);
701         Intent batteryIntent =
702                 createIntentForGetBatteryInfoTest(
703                         ChargingType.WIRELESS, ChargingSpeed.REGULAR, /* batteryLevel= */ 10);
704         when(mFeatureFactory.batterySettingsFeatureProvider.getWirelessChargingRemainingLabel(
705                         eq(mContext), anyLong(), anyLong()))
706                 .thenReturn(null);
707         var expectedStatusLabel = "Charging wirelessly";
708         var expectedRemainingLabel = "2 hr, 10 min left until full";
709         var expectedChargeLabel = "10% - " + expectedRemainingLabel;
710 
711         assertGetBatteryInfo(
712                 batteryIntent,
713                 /* currentTimeMillis= */ UNUSED_TIME_MS,
714                 expectedStatusLabel,
715                 expectedRemainingLabel,
716                 expectedChargeLabel);
717     }
718 
719     @Test
getBatteryInfo_chargeOptimizationMode_updateRemainingAndStatusLabel()720     public void getBatteryInfo_chargeOptimizationMode_updateRemainingAndStatusLabel() {
721         prepareTestGetBatteryInfoEnvironment(
722                 /* remainingTimeMs= */ Duration.ofMinutes(130).toMillis(),
723                 /* chargingStringV2Enabled= */ false);
724         Intent batteryIntent =
725                 createIntentForGetBatteryInfoTest(
726                         ChargingType.WIRED, ChargingSpeed.REGULAR, /* batteryLevel= */ 65);
727         var expectedRemainingLabel = "Expected remaining label";
728         var expectedChargeLabel = "65% - " + expectedRemainingLabel;
729         when(mFeatureFactory.batterySettingsFeatureProvider.isChargingOptimizationMode(mContext))
730                 .thenReturn(true);
731         when(mFeatureFactory.batterySettingsFeatureProvider.getChargingOptimizationRemainingLabel(
732                         eq(mContext), anyInt(), anyInt(), anyLong(), anyLong()))
733                 .thenReturn(expectedRemainingLabel);
734         when(mFeatureFactory.batterySettingsFeatureProvider.getChargingOptimizationChargeLabel(
735                         eq(mContext), anyInt(), anyString(), anyLong(), anyLong()))
736                 .thenReturn(expectedChargeLabel);
737         var expectedStatusLabel = "Charging";
738 
739         assertGetBatteryInfo(
740                 batteryIntent,
741                 /* currentTimeMillis= */ UNUSED_TIME_MS,
742                 expectedStatusLabel,
743                 expectedRemainingLabel,
744                 expectedChargeLabel);
745     }
746 
747     @Test
getBatteryInfo_notChargeOptimizationModeWithV1_updateRemainingAndStatusLabel()748     public void getBatteryInfo_notChargeOptimizationModeWithV1_updateRemainingAndStatusLabel() {
749         prepareTestGetBatteryInfoEnvironment(
750                 /* remainingTimeMs= */ Duration.ofMinutes(130).toMillis(),
751                 /* chargingStringV2Enabled= */ false);
752         Intent batteryIntent =
753                 createIntentForGetBatteryInfoTest(
754                         ChargingType.WIRED, ChargingSpeed.REGULAR, /* batteryLevel= */ 65);
755         when(mFeatureFactory.batterySettingsFeatureProvider.isChargingOptimizationMode(mContext))
756                 .thenReturn(false);
757         var expectedStatusLabel = "Charging";
758         var expectedRemainingLabel = "2 hr, 10 min left until full";
759         var expectedChargeLabel = "65% - " + expectedRemainingLabel;
760 
761         assertGetBatteryInfo(
762                 batteryIntent,
763                 /* currentTimeMillis= */ UNUSED_TIME_MS,
764                 expectedStatusLabel,
765                 expectedRemainingLabel,
766                 expectedChargeLabel);
767     }
768 
769     @Test
getBatteryInfo_notChargeOptimizationModeWithV2_updateRemainingAndStatusLabel()770     public void getBatteryInfo_notChargeOptimizationModeWithV2_updateRemainingAndStatusLabel() {
771         prepareTestGetBatteryInfoEnvironment(
772                 /* remainingTimeMs= */ Duration.ofMinutes(130).toMillis(),
773                 /* chargingStringV2Enabled= */ true);
774         Intent batteryIntent =
775                 createIntentForGetBatteryInfoTest(
776                         ChargingType.WIRED, ChargingSpeed.REGULAR, /* batteryLevel= */ 65);
777         when(mFeatureFactory.batterySettingsFeatureProvider.isChargingOptimizationMode(mContext))
778                 .thenReturn(false);
779         var expectedStatusLabel = "Charging";
780         var expectedRemainingLabel = "Fully charged by";
781         var expectedChargeLabel = "65% - " + expectedRemainingLabel;
782         var currentTimeMillis = Instant.parse("2024-04-01T15:00:00Z").toEpochMilli();
783 
784         assertGetBatteryInfo(
785                 batteryIntent,
786                 currentTimeMillis,
787                 expectedStatusLabel,
788                 expectedRemainingLabel,
789                 expectedChargeLabel);
790     }
791 
792     private enum ChargingSpeed {
793         FAST,
794         REGULAR,
795         SLOW
796     }
797 
798     private enum ChargingType {
799         WIRED,
800         WIRELESS,
801         DOCKED
802     }
803 
createIntentForGetBatteryInfoTest( ChargingType chargingType, ChargingSpeed chargingSpeed, int batteryLevel)804     private Intent createIntentForGetBatteryInfoTest(
805             ChargingType chargingType, ChargingSpeed chargingSpeed, int batteryLevel) {
806         return createBatteryIntent(
807                         CHARGING_TYPE_MAP.get(chargingType),
808                         batteryLevel,
809                         BatteryManager.BATTERY_STATUS_CHARGING)
810                 .putExtra(
811                         BatteryManager.EXTRA_MAX_CHARGING_CURRENT,
812                         CHARGING_SPEED_MAP.get(chargingSpeed))
813                 .putExtra(BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE, 5000000);
814     }
815 
prepareTestGetBatteryInfoEnvironment( long remainingTimeMs, boolean chargingStringV2Enabled)816     private void prepareTestGetBatteryInfoEnvironment(
817             long remainingTimeMs, boolean chargingStringV2Enabled) {
818         when(mBatteryUsageStats.getChargeTimeRemainingMs()).thenReturn(remainingTimeMs);
819         SystemProperties.set(
820                 com.android.settingslib.fuelgauge.BatteryUtils.PROPERTY_CHARGING_STRING_V2_KEY,
821                 String.valueOf(chargingStringV2Enabled));
822         Settings.Global.putInt(
823                 mContext.getContentResolver(),
824                 BatteryUtils.SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS,
825                 1);
826     }
827 
assertGetBatteryInfo( Intent batteryIntent, long currentTimeMillis, String expectedStatusLabel, String expectedRemainingLabel, String expectedChargeLabel)828     private void assertGetBatteryInfo(
829             Intent batteryIntent,
830             long currentTimeMillis,
831             String expectedStatusLabel,
832             String expectedRemainingLabel,
833             String expectedChargeLabel) {
834         mContext.getResources().getConfiguration().setLocale(Locale.US);
835         TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
836         var info =
837                 BatteryInfo.getBatteryInfo(
838                         mContext,
839                         batteryIntent,
840                         mBatteryUsageStats,
841                         MOCK_ESTIMATE,
842                         /* elapsedRealtimeUs= */ UNUSED_TIME_MS,
843                         /* shortString= */ false,
844                         /* currentTimeMillis= */ currentTimeMillis);
845 
846         assertWithMessage("statusLabel is incorrect")
847                 .that(info.statusLabel)
848                 .isEqualTo(expectedStatusLabel);
849         assertWithMessage("remainingLabel is incorrect")
850                 .that(info.remainingLabel.toString())
851                 .contains(expectedRemainingLabel);
852         assertWithMessage("chargeLabel is incorrect")
853                 .that(info.chargeLabel.toString())
854                 .contains(expectedChargeLabel);
855     }
856 
createBatteryIntent(int plugged, int level, int status)857     private static Intent createBatteryIntent(int plugged, int level, int status) {
858         return new Intent()
859                 .putExtra(BatteryManager.EXTRA_PLUGGED, plugged)
860                 .putExtra(BatteryManager.EXTRA_LEVEL, level)
861                 .putExtra(BatteryManager.EXTRA_SCALE, 100)
862                 .putExtra(BatteryManager.EXTRA_STATUS, status);
863     }
864 
865     // Make our battery stats return a sequence of battery events.
mockBatteryStatsHistory()866     private void mockBatteryStatsHistory() {
867         // Mock out new data every time iterateBatteryStatsHistory is called.
868         doAnswer(
869                         invocation -> {
870                             BatteryStatsHistoryIterator iterator =
871                                     mock(BatteryStatsHistoryIterator.class);
872                             when(iterator.next())
873                                     .thenReturn(
874                                             makeHistoryIterm(1000, 99),
875                                             makeHistoryIterm(1500, 98),
876                                             makeHistoryIterm(2000, 97),
877                                             null);
878                             return iterator;
879                         })
880                 .when(mBatteryUsageStats)
881                 .iterateBatteryStatsHistory();
882     }
883 
makeHistoryIterm(long time, int batteryLevel)884     private BatteryStats.HistoryItem makeHistoryIterm(long time, int batteryLevel) {
885         BatteryStats.HistoryItem record = new BatteryStats.HistoryItem();
886         record.cmd = BatteryStats.HistoryItem.CMD_UPDATE;
887         record.time = time;
888         record.batteryLevel = (byte) batteryLevel;
889         return record;
890     }
891 
assertOnlyHistory(BatteryInfo info)892     private void assertOnlyHistory(BatteryInfo info) {
893         mockBatteryStatsHistory();
894         UsageView view = mock(UsageView.class);
895         when(view.getContext()).thenReturn(mContext);
896 
897         info.bindHistory(view);
898         verify(view, times(1)).configureGraph(anyInt(), anyInt());
899         verify(view, times(1)).addPath(any(SparseIntArray.class));
900         verify(view, never()).addProjectedPath(any(SparseIntArray.class));
901     }
902 
assertHistoryAndLinearProjection(BatteryInfo info)903     private void assertHistoryAndLinearProjection(BatteryInfo info) {
904         mockBatteryStatsHistory();
905         UsageView view = mock(UsageView.class);
906         when(view.getContext()).thenReturn(mContext);
907 
908         info.bindHistory(view);
909         verify(view, times(2)).configureGraph(anyInt(), anyInt());
910         verify(view, times(1)).addPath(any(SparseIntArray.class));
911         ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class);
912         verify(view, times(1)).addProjectedPath(pointsActual.capture());
913 
914         // Check that we have two points and the first is correct.
915         assertThat(pointsActual.getValue().size()).isEqualTo(2);
916         assertThat(pointsActual.getValue().keyAt(0)).isEqualTo(2000);
917         assertThat(pointsActual.getValue().valueAt(0)).isEqualTo(97);
918     }
919 
assertHistoryAndEnhancedProjection(BatteryInfo info)920     private void assertHistoryAndEnhancedProjection(BatteryInfo info) {
921         mockBatteryStatsHistory();
922         UsageView view = mock(UsageView.class);
923         when(view.getContext()).thenReturn(mContext);
924         SparseIntArray pointsExpected = new SparseIntArray();
925         pointsExpected.append(2000, 96);
926         pointsExpected.append(2500, 95);
927         pointsExpected.append(3000, 94);
928         doReturn(pointsExpected)
929                 .when(mFeatureFactory.powerUsageFeatureProvider)
930                 .getEnhancedBatteryPredictionCurve(any(Context.class), anyLong());
931 
932         info.bindHistory(view);
933         verify(view, times(2)).configureGraph(anyInt(), anyInt());
934         verify(view, times(1)).addPath(any(SparseIntArray.class));
935         ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class);
936         verify(view, times(1)).addProjectedPath(pointsActual.capture());
937         assertThat(pointsActual.getValue()).isEqualTo(pointsExpected);
938     }
939 
getBatteryInfo(boolean charging, boolean enhanced, boolean estimate)940     private BatteryInfo getBatteryInfo(boolean charging, boolean enhanced, boolean estimate) {
941         if (charging && estimate) {
942             doReturn(1000L).when(mBatteryUsageStats).getChargeTimeRemainingMs();
943         } else {
944             doReturn(0L).when(mBatteryUsageStats).getChargeTimeRemainingMs();
945         }
946         Estimate batteryEstimate =
947                 new Estimate(
948                         estimate ? 1000 : 0,
949                         false /* isBasedOnUsage */,
950                         1000 /* averageDischargeTime */);
951         BatteryInfo info =
952                 BatteryInfo.getBatteryInfo(
953                         mContext,
954                         charging ? mChargingBatteryBroadcast : mDisChargingBatteryBroadcast,
955                         mBatteryUsageStats,
956                         batteryEstimate,
957                         SystemClock.elapsedRealtime() * 1000,
958                         false);
959         doReturn(enhanced)
960                 .when(mFeatureFactory.powerUsageFeatureProvider)
961                 .isEnhancedBatteryPredictionEnabled(mContext);
962         return info;
963     }
964 
965     @Test
testBindHistory()966     public void testBindHistory() {
967         BatteryInfo info;
968 
969         info = getBatteryInfo(false /* charging */, false /* enhanced */, false /* estimate */);
970         assertOnlyHistory(info);
971 
972         info = getBatteryInfo(false /* charging */, false /* enhanced */, true /* estimate */);
973         assertHistoryAndLinearProjection(info);
974 
975         info = getBatteryInfo(false /* charging */, true /* enhanced */, false /* estimate */);
976         assertOnlyHistory(info);
977 
978         info = getBatteryInfo(false /* charging */, true /* enhanced */, true /* estimate */);
979         assertHistoryAndEnhancedProjection(info);
980 
981         info = getBatteryInfo(true /* charging */, false /* enhanced */, false /* estimate */);
982         assertOnlyHistory(info);
983 
984         info = getBatteryInfo(true /* charging */, false /* enhanced */, true /* estimate */);
985         assertHistoryAndLinearProjection(info);
986 
987         info = getBatteryInfo(true /* charging */, true /* enhanced */, false /* estimate */);
988         assertOnlyHistory(info);
989 
990         // Linear projection for charging even in enhanced mode.
991         info = getBatteryInfo(true /* charging */, true /* enhanced */, true /* estimate */);
992         assertHistoryAndLinearProjection(info);
993     }
994 }
995