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.SystemClock; 38 import android.util.SparseIntArray; 39 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; 44 import com.android.settingslib.fuelgauge.Estimate; 45 46 import org.junit.Before; 47 import org.junit.Test; 48 import org.junit.runner.RunWith; 49 import org.mockito.Answers; 50 import org.mockito.ArgumentCaptor; 51 import org.mockito.Mock; 52 import org.mockito.MockitoAnnotations; 53 import org.mockito.invocation.InvocationOnMock; 54 import org.mockito.stubbing.Answer; 55 import org.robolectric.RobolectricTestRunner; 56 import org.robolectric.RuntimeEnvironment; 57 58 import java.time.Duration; 59 import java.util.concurrent.TimeUnit; 60 61 @RunWith(RobolectricTestRunner.class) 62 public class BatteryInfoTest { 63 64 private static final String STATUS_CHARGING_NO_TIME = "50% - charging"; 65 private static final String STATUS_CHARGING_TIME = "50% - 0 min until fully charged"; 66 private static final String STATUS_NOT_CHARGING = "Not charging"; 67 private static final long REMAINING_TIME_NULL = -1; 68 private static final long REMAINING_TIME = 2; 69 // Strings are defined in frameworks/base/packages/SettingsLib/res/values/strings.xml 70 private static final String ENHANCED_STRING_SUFFIX = "based on your usage"; 71 private static final String EXTEND_PREFIX = "Extend battery life past"; 72 private static final long TEST_CHARGE_TIME_REMAINING = TimeUnit.MINUTES.toMicros(1); 73 private static final String TEST_CHARGE_TIME_REMAINING_STRINGIFIED = 74 "1 min left until fully charged"; 75 private static final String TEST_BATTERY_LEVEL_10 = "10%"; 76 private static final String FIFTEEN_MIN_FORMATTED = "15 min"; 77 private static final Estimate DUMMY_ESTIMATE = new Estimate( 78 1000, /* estimateMillis */ 79 false, /* isBasedOnUsage */ 80 1000 /* averageDischargeTime */); 81 82 private Intent mDisChargingBatteryBroadcast; 83 private Intent mChargingBatteryBroadcast; 84 private Context mContext; 85 private FakeFeatureFactory mFeatureFactory; 86 87 @Mock(answer = Answers.RETURNS_DEEP_STUBS) 88 private BatteryStats mBatteryStats; 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(mBatteryStats).computeBatteryTimeRemaining(anyLong()); 104 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, 105 mDisChargingBatteryBroadcast, mBatteryStats, SystemClock.elapsedRealtime() * 1000, 106 true /* shortString */); 107 108 assertThat(info.statusLabel).isEqualTo(STATUS_NOT_CHARGING); 109 } 110 111 @Test testGetBatteryInfo_doNotShowChargingMethod_hasRemainingTime()112 public void testGetBatteryInfo_doNotShowChargingMethod_hasRemainingTime() { 113 doReturn(REMAINING_TIME).when(mBatteryStats).computeChargeTimeRemaining(anyLong()); 114 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast, 115 mBatteryStats, SystemClock.elapsedRealtime() * 1000, false /* shortString */); 116 117 assertThat(info.chargeLabel.toString()).isEqualTo(STATUS_CHARGING_TIME); 118 } 119 120 @Test testGetBatteryInfo_doNotShowChargingMethod_noRemainingTime()121 public void testGetBatteryInfo_doNotShowChargingMethod_noRemainingTime() { 122 doReturn(REMAINING_TIME_NULL).when(mBatteryStats).computeChargeTimeRemaining(anyLong()); 123 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast, 124 mBatteryStats, SystemClock.elapsedRealtime() * 1000, false /* shortString */); 125 126 assertThat(info.chargeLabel.toString()).isEqualTo(STATUS_CHARGING_NO_TIME); 127 } 128 129 @Test testGetBatteryInfo_pluggedInUsingShortString_usesCorrectData()130 public void testGetBatteryInfo_pluggedInUsingShortString_usesCorrectData() { 131 doReturn(TEST_CHARGE_TIME_REMAINING).when(mBatteryStats).computeChargeTimeRemaining( 132 anyLong()); 133 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast, 134 mBatteryStats, SystemClock.elapsedRealtime() * 1000, true /* shortString */); 135 136 assertThat(info.discharging).isEqualTo(false); 137 assertThat(info.chargeLabel.toString()).isEqualTo("50% - 1 min until fully charged"); 138 } 139 140 @Test testGetBatteryInfo_basedOnUsageTrueMoreThanFifteenMinutes_usesCorrectString()141 public void testGetBatteryInfo_basedOnUsageTrueMoreThanFifteenMinutes_usesCorrectString() { 142 Estimate estimate = new Estimate(Duration.ofHours(4).toMillis(), 143 true /* isBasedOnUsage */, 144 1000 /* averageDischargeTime */); 145 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 146 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000, 147 false /* shortString */); 148 BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 149 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000, 150 true /* shortString */); 151 152 // We only add special mention for the long string 153 assertThat(info.remainingLabel.toString()).contains(ENHANCED_STRING_SUFFIX); 154 assertThat(info.suggestionLabel).contains(EXTEND_PREFIX); 155 // shortened string should not have extra text 156 assertThat(info2.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX); 157 assertThat(info2.suggestionLabel).contains(EXTEND_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 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000, 167 false /* shortString */); 168 BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 169 mBatteryStats, 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(EXTEND_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 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000, 187 false /* shortString */); 188 189 assertThat(info.suggestionLabel).doesNotContain(EXTEND_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 mBatteryStats, 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 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000, 215 false /* shortString */); 216 BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 217 mBatteryStats, DUMMY_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) 227 .when(mBatteryStats) 228 .computeChargeTimeRemaining(anyLong()); 229 230 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mChargingBatteryBroadcast, 231 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000, 232 false /* shortString */); 233 assertThat(info.remainingTimeUs).isEqualTo(TEST_CHARGE_TIME_REMAINING); 234 assertThat(info.remainingLabel.toString()) 235 .isEqualTo(TEST_CHARGE_TIME_REMAINING_STRINGIFIED); 236 } 237 238 @Test testGetBatteryInfo_pluggedInWithFullBattery_onlyShowBatteryLevel()239 public void testGetBatteryInfo_pluggedInWithFullBattery_onlyShowBatteryLevel() { 240 mChargingBatteryBroadcast.putExtra(BatteryManager.EXTRA_LEVEL, 100); 241 242 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mChargingBatteryBroadcast, 243 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000, 244 false /* shortString */); 245 246 assertThat(info.chargeLabel).isEqualTo("100%"); 247 } 248 249 // Make our battery stats return a sequence of battery events. mockBatteryStatsHistory()250 private void mockBatteryStatsHistory() { 251 // Mock out new data every time start...Locked is called. 252 doAnswer(invocation -> { 253 doAnswer(new Answer() { 254 private int count = 0; 255 private long[] times = {1000, 1500, 2000}; 256 private byte[] levels = {99, 98, 97}; 257 258 @Override 259 public Object answer(InvocationOnMock invocation) throws Throwable { 260 if (count == times.length) { 261 return false; 262 } 263 BatteryStats.HistoryItem record = invocation.getArgument(0); 264 record.cmd = BatteryStats.HistoryItem.CMD_UPDATE; 265 record.time = times[count]; 266 record.batteryLevel = levels[count]; 267 count++; 268 return true; 269 } 270 }).when(mBatteryStats).getNextHistoryLocked(any(BatteryStats.HistoryItem.class)); 271 return true; 272 }).when(mBatteryStats).startIteratingHistoryLocked(); 273 } 274 assertOnlyHistory(BatteryInfo info)275 private void assertOnlyHistory(BatteryInfo info) { 276 mockBatteryStatsHistory(); 277 UsageView view = mock(UsageView.class); 278 when(view.getContext()).thenReturn(mContext); 279 280 info.bindHistory(view); 281 verify(view, times(1)).configureGraph(anyInt(), anyInt()); 282 verify(view, times(1)).addPath(any(SparseIntArray.class)); 283 verify(view, never()).addProjectedPath(any(SparseIntArray.class)); 284 } 285 assertHistoryAndLinearProjection(BatteryInfo info)286 private void assertHistoryAndLinearProjection(BatteryInfo info) { 287 mockBatteryStatsHistory(); 288 UsageView view = mock(UsageView.class); 289 when(view.getContext()).thenReturn(mContext); 290 291 info.bindHistory(view); 292 verify(view, times(2)).configureGraph(anyInt(), anyInt()); 293 verify(view, times(1)).addPath(any(SparseIntArray.class)); 294 ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class); 295 verify(view, times(1)).addProjectedPath(pointsActual.capture()); 296 297 // Check that we have two points and the first is correct. 298 assertThat(pointsActual.getValue().size()).isEqualTo(2); 299 assertThat(pointsActual.getValue().keyAt(0)).isEqualTo(2000); 300 assertThat(pointsActual.getValue().valueAt(0)).isEqualTo(97); 301 } 302 assertHistoryAndEnhancedProjection(BatteryInfo info)303 private void assertHistoryAndEnhancedProjection(BatteryInfo info) { 304 mockBatteryStatsHistory(); 305 UsageView view = mock(UsageView.class); 306 when(view.getContext()).thenReturn(mContext); 307 SparseIntArray pointsExpected = new SparseIntArray(); 308 pointsExpected.append(2000, 96); 309 pointsExpected.append(2500, 95); 310 pointsExpected.append(3000, 94); 311 doReturn(pointsExpected).when(mFeatureFactory.powerUsageFeatureProvider) 312 .getEnhancedBatteryPredictionCurve(any(Context.class), anyLong()); 313 314 info.bindHistory(view); 315 verify(view, times(2)).configureGraph(anyInt(), anyInt()); 316 verify(view, times(1)).addPath(any(SparseIntArray.class)); 317 ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class); 318 verify(view, times(1)).addProjectedPath(pointsActual.capture()); 319 assertThat(pointsActual.getValue()).isEqualTo(pointsExpected); 320 } 321 getBatteryInfo(boolean charging, boolean enhanced, boolean estimate)322 private BatteryInfo getBatteryInfo(boolean charging, boolean enhanced, boolean estimate) { 323 if (charging && estimate) { 324 doReturn(1000L).when(mBatteryStats).computeChargeTimeRemaining(anyLong()); 325 } else { 326 doReturn(0L).when(mBatteryStats).computeChargeTimeRemaining(anyLong()); 327 } 328 Estimate batteryEstimate = new Estimate( 329 estimate ? 1000 : 0, 330 false /* isBasedOnUsage */, 331 1000 /* averageDischargeTime */); 332 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, 333 charging ? mChargingBatteryBroadcast : mDisChargingBatteryBroadcast, 334 mBatteryStats, batteryEstimate, SystemClock.elapsedRealtime() * 1000, false); 335 doReturn(enhanced).when(mFeatureFactory.powerUsageFeatureProvider) 336 .isEnhancedBatteryPredictionEnabled(mContext); 337 return info; 338 } 339 340 @Test testBindHistory()341 public void testBindHistory() { 342 BatteryInfo info; 343 344 info = getBatteryInfo(false /* charging */, false /* enhanced */, false /* estimate */); 345 assertOnlyHistory(info); 346 347 info = getBatteryInfo(false /* charging */, false /* enhanced */, true /* estimate */); 348 assertHistoryAndLinearProjection(info); 349 350 info = getBatteryInfo(false /* charging */, true /* enhanced */, false /* estimate */); 351 assertOnlyHistory(info); 352 353 info = getBatteryInfo(false /* charging */, true /* enhanced */, true /* estimate */); 354 assertHistoryAndEnhancedProjection(info); 355 356 info = getBatteryInfo(true /* charging */, false /* enhanced */, false /* estimate */); 357 assertOnlyHistory(info); 358 359 info = getBatteryInfo(true /* charging */, false /* enhanced */, true /* estimate */); 360 assertHistoryAndLinearProjection(info); 361 362 info = getBatteryInfo(true /* charging */, true /* enhanced */, false /* estimate */); 363 assertOnlyHistory(info); 364 365 // Linear projection for charging even in enhanced mode. 366 info = getBatteryInfo(true /* charging */, true /* enhanced */, true /* estimate */); 367 assertHistoryAndLinearProjection(info); 368 } 369 } 370