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