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 org.mockito.ArgumentMatchers.anyInt; 21 import static org.mockito.Matchers.anyLong; 22 import static org.mockito.Mockito.any; 23 import static org.mockito.Mockito.doAnswer; 24 import static org.mockito.Mockito.doReturn; 25 import static org.mockito.Mockito.mock; 26 import static org.mockito.Mockito.never; 27 import static org.mockito.Mockito.spy; 28 import static org.mockito.Mockito.times; 29 import static org.mockito.Mockito.verify; 30 import static org.mockito.Mockito.when; 31 32 import android.content.Context; 33 import android.content.Intent; 34 import android.os.BatteryManager; 35 import android.os.BatteryStats; 36 import android.os.SystemClock; 37 import android.util.SparseIntArray; 38 39 import com.android.settings.graph.UsageView; 40 import com.android.settings.testutils.BatteryTestUtils; 41 import com.android.settings.testutils.FakeFeatureFactory; 42 import com.android.settings.testutils.SettingsRobolectricTestRunner; 43 import com.android.settingslib.R; 44 import com.android.settingslib.utils.PowerUtil; 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.RuntimeEnvironment; 56 57 import java.time.Duration; 58 import java.util.concurrent.TimeUnit; 59 60 @RunWith(SettingsRobolectricTestRunner.class) 61 public class BatteryInfoTest { 62 63 private static final String STATUS_CHARGING_NO_TIME = "50% - charging"; 64 private static final String STATUS_CHARGING_TIME = "50% - 0 min until fully charged"; 65 private static final String STATUS_NOT_CHARGING = "Not charging"; 66 private static final long REMAINING_TIME_NULL = -1; 67 private static final long REMAINING_TIME = 2; 68 private static final String ENHANCED_STRING_SUFFIX = "based on your usage"; 69 private static final long TEST_CHARGE_TIME_REMAINING = TimeUnit.MINUTES.toMicros(1); 70 private static final String TEST_CHARGE_TIME_REMAINING_STRINGIFIED = 71 "1 min left until fully charged"; 72 private static final String TEST_BATTERY_LEVEL_10 = "10%"; 73 private static final String FIFTEEN_MIN_FORMATTED = "15 min"; 74 public static final Estimate DUMMY_ESTIMATE = new Estimate( 75 1000, /* estimateMillis */ 76 false, /* isBasedOnUsage */ 77 1000 /* averageDischargeTime */); 78 79 private Intent mDisChargingBatteryBroadcast; 80 private Intent mChargingBatteryBroadcast; 81 private Context mContext; 82 private FakeFeatureFactory mFeatureFactory; 83 84 @Mock(answer = Answers.RETURNS_DEEP_STUBS) 85 private BatteryStats mBatteryStats; 86 87 @Before 88 public void setUp() { 89 MockitoAnnotations.initMocks(this); 90 mContext = spy(RuntimeEnvironment.application); 91 mFeatureFactory = FakeFeatureFactory.setupForTest(); 92 93 mDisChargingBatteryBroadcast = BatteryTestUtils.getDischargingIntent(); 94 95 mChargingBatteryBroadcast = BatteryTestUtils.getChargingIntent(); 96 } 97 98 @Test 99 public void testGetBatteryInfo_hasStatusLabel() { 100 doReturn(REMAINING_TIME_NULL).when(mBatteryStats).computeBatteryTimeRemaining(anyLong()); 101 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, 102 mDisChargingBatteryBroadcast, mBatteryStats, SystemClock.elapsedRealtime() * 1000, 103 true /* shortString */); 104 105 assertThat(info.statusLabel).isEqualTo(STATUS_NOT_CHARGING); 106 } 107 108 @Test 109 public void testGetBatteryInfo_doNotShowChargingMethod_hasRemainingTime() { 110 doReturn(REMAINING_TIME).when(mBatteryStats).computeChargeTimeRemaining(anyLong()); 111 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast, 112 mBatteryStats, SystemClock.elapsedRealtime() * 1000, false /* shortString */); 113 114 assertThat(info.chargeLabel.toString()).isEqualTo(STATUS_CHARGING_TIME); 115 } 116 117 @Test 118 public void testGetBatteryInfo_doNotShowChargingMethod_noRemainingTime() { 119 doReturn(REMAINING_TIME_NULL).when(mBatteryStats).computeChargeTimeRemaining(anyLong()); 120 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast, 121 mBatteryStats, SystemClock.elapsedRealtime() * 1000, false /* shortString */); 122 123 assertThat(info.chargeLabel.toString()).isEqualTo(STATUS_CHARGING_NO_TIME); 124 } 125 126 @Test 127 public void testGetBatteryInfo_pluggedInUsingShortString_usesCorrectData() { 128 doReturn(TEST_CHARGE_TIME_REMAINING).when(mBatteryStats).computeChargeTimeRemaining( 129 anyLong()); 130 BatteryInfo info = BatteryInfo.getBatteryInfoOld(mContext, mChargingBatteryBroadcast, 131 mBatteryStats, SystemClock.elapsedRealtime() * 1000, true /* shortString */); 132 133 assertThat(info.discharging).isEqualTo(false); 134 assertThat(info.chargeLabel.toString()).isEqualTo("50% - 1 min until fully charged"); 135 } 136 137 @Test 138 public void testGetBatteryInfo_basedOnUsageTrueMoreThanFifteenMinutes_usesCorrectString() { 139 Estimate estimate = new Estimate(Duration.ofHours(4).toMillis(), 140 true /* isBasedOnUsage */, 141 1000 /* averageDischargeTime */); 142 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 143 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000, 144 false /* shortString */); 145 BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 146 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000, 147 true /* shortString */); 148 149 // We only add special mention for the long string 150 assertThat(info.remainingLabel.toString()).contains(ENHANCED_STRING_SUFFIX); 151 // shortened string should not have extra text 152 assertThat(info2.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX); 153 } 154 155 @Test 156 public void testGetBatteryInfo_basedOnUsageTrueLessThanSevenMinutes_usesCorrectString() { 157 Estimate estimate = new Estimate(Duration.ofMinutes(7).toMillis(), 158 true /* isBasedOnUsage */, 159 1000 /* averageDischargeTime */); 160 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 161 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000, 162 false /* shortString */); 163 BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 164 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000, 165 true /* shortString */); 166 167 // These should be identical in either case 168 assertThat(info.remainingLabel.toString()).isEqualTo( 169 mContext.getString(R.string.power_remaining_duration_only_shutdown_imminent)); 170 assertThat(info2.remainingLabel.toString()).isEqualTo( 171 mContext.getString(R.string.power_remaining_duration_only_shutdown_imminent)); 172 } 173 174 @Test 175 public void testGetBatteryInfo_basedOnUsageTrueBetweenSevenAndFifteenMinutes_usesCorrectString() { 176 Estimate estimate = new Estimate(Duration.ofMinutes(10).toMillis(), 177 true /* isBasedOnUsage */, 178 1000 /* averageDischargeTime */); 179 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 180 mBatteryStats, estimate, SystemClock.elapsedRealtime() * 1000, 181 false /* shortString */); 182 183 // Check that strings are showing less than 15 minutes remaining regardless of exact time. 184 assertThat(info.chargeLabel.toString()).isEqualTo( 185 mContext.getString(R.string.power_remaining_less_than_duration, 186 FIFTEEN_MIN_FORMATTED, TEST_BATTERY_LEVEL_10)); 187 assertThat(info.remainingLabel.toString()).isEqualTo( 188 mContext.getString(R.string.power_remaining_less_than_duration_only, 189 FIFTEEN_MIN_FORMATTED)); 190 } 191 192 @Test 193 public void testGetBatteryInfo_basedOnUsageFalse_usesDefaultString() { 194 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 195 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000, 196 false /* shortString */); 197 BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, 198 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000, 199 true /* shortString */); 200 201 assertThat(info.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX); 202 assertThat(info2.remainingLabel.toString()).doesNotContain(ENHANCED_STRING_SUFFIX); 203 } 204 205 @Test 206 public void testGetBatteryInfo_charging_usesChargeTime() { 207 doReturn(TEST_CHARGE_TIME_REMAINING) 208 .when(mBatteryStats) 209 .computeChargeTimeRemaining(anyLong()); 210 211 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mChargingBatteryBroadcast, 212 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000, 213 false /* shortString */); 214 assertThat(info.remainingTimeUs).isEqualTo(TEST_CHARGE_TIME_REMAINING); 215 assertThat(info.remainingLabel.toString()) 216 .isEqualTo(TEST_CHARGE_TIME_REMAINING_STRINGIFIED); 217 } 218 219 @Test 220 public void testGetBatteryInfo_pluggedInWithFullBattery_onlyShowBatteryLevel() { 221 mChargingBatteryBroadcast.putExtra(BatteryManager.EXTRA_LEVEL, 100); 222 223 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mChargingBatteryBroadcast, 224 mBatteryStats, DUMMY_ESTIMATE, SystemClock.elapsedRealtime() * 1000, 225 false /* shortString */); 226 227 assertThat(info.chargeLabel).isEqualTo("100%"); 228 } 229 230 // Make our battery stats return a sequence of battery events. 231 private void mockBatteryStatsHistory() { 232 // Mock out new data every time start...Locked is called. 233 doAnswer(invocation -> { 234 doAnswer(new Answer() { 235 private int count = 0; 236 private long[] times = {1000, 1500, 2000}; 237 private byte[] levels = {99, 98, 97}; 238 239 @Override 240 public Object answer(InvocationOnMock invocation) throws Throwable { 241 if (count == times.length) { 242 return false; 243 } 244 BatteryStats.HistoryItem record = invocation.getArgument(0); 245 record.cmd = BatteryStats.HistoryItem.CMD_UPDATE; 246 record.time = times[count]; 247 record.batteryLevel = levels[count]; 248 count++; 249 return true; 250 } 251 }).when(mBatteryStats).getNextHistoryLocked(any(BatteryStats.HistoryItem.class)); 252 return true; 253 }).when(mBatteryStats).startIteratingHistoryLocked(); 254 } 255 256 private void assertOnlyHistory(BatteryInfo info) { 257 mockBatteryStatsHistory(); 258 UsageView view = mock(UsageView.class); 259 when(view.getContext()).thenReturn(mContext); 260 261 info.bindHistory(view); 262 verify(view, times(1)).configureGraph(anyInt(), anyInt()); 263 verify(view, times(1)).addPath(any(SparseIntArray.class)); 264 verify(view, never()).addProjectedPath(any(SparseIntArray.class)); 265 } 266 267 private void assertHistoryAndLinearProjection(BatteryInfo info) { 268 mockBatteryStatsHistory(); 269 UsageView view = mock(UsageView.class); 270 when(view.getContext()).thenReturn(mContext); 271 272 info.bindHistory(view); 273 verify(view, times(2)).configureGraph(anyInt(), anyInt()); 274 verify(view, times(1)).addPath(any(SparseIntArray.class)); 275 ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class); 276 verify(view, times(1)).addProjectedPath(pointsActual.capture()); 277 278 // Check that we have two points and the first is correct. 279 assertThat(pointsActual.getValue().size()).isEqualTo(2); 280 assertThat(pointsActual.getValue().keyAt(0)).isEqualTo(2000); 281 assertThat(pointsActual.getValue().valueAt(0)).isEqualTo(97); 282 } 283 284 private void assertHistoryAndEnhancedProjection(BatteryInfo info) { 285 mockBatteryStatsHistory(); 286 UsageView view = mock(UsageView.class); 287 when(view.getContext()).thenReturn(mContext); 288 SparseIntArray pointsExpected = new SparseIntArray(); 289 pointsExpected.append(2000, 96); 290 pointsExpected.append(2500, 95); 291 pointsExpected.append(3000, 94); 292 doReturn(pointsExpected).when(mFeatureFactory.powerUsageFeatureProvider) 293 .getEnhancedBatteryPredictionCurve(any(Context.class), anyLong()); 294 295 info.bindHistory(view); 296 verify(view, times(2)).configureGraph(anyInt(), anyInt()); 297 verify(view, times(1)).addPath(any(SparseIntArray.class)); 298 ArgumentCaptor<SparseIntArray> pointsActual = ArgumentCaptor.forClass(SparseIntArray.class); 299 verify(view, times(1)).addProjectedPath(pointsActual.capture()); 300 assertThat(pointsActual.getValue()).isEqualTo(pointsExpected); 301 } 302 303 private BatteryInfo getBatteryInfo(boolean charging, boolean enhanced, boolean estimate) { 304 if (charging && estimate) { 305 doReturn(1000L).when(mBatteryStats).computeChargeTimeRemaining(anyLong()); 306 } else { 307 doReturn(0L).when(mBatteryStats).computeChargeTimeRemaining(anyLong()); 308 } 309 Estimate batteryEstimate = new Estimate( 310 estimate ? 1000 : 0, 311 false /* isBasedOnUsage */, 312 1000 /* averageDischargeTime */); 313 BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, 314 charging ? mChargingBatteryBroadcast : mDisChargingBatteryBroadcast, 315 mBatteryStats, batteryEstimate, SystemClock.elapsedRealtime() * 1000, false); 316 doReturn(enhanced).when(mFeatureFactory.powerUsageFeatureProvider) 317 .isEnhancedBatteryPredictionEnabled(mContext); 318 return info; 319 } 320 321 @Test 322 public void testBindHistory() { 323 BatteryInfo info; 324 325 info = getBatteryInfo(false /* charging */, false /* enhanced */, false /* estimate */); 326 assertOnlyHistory(info); 327 328 info = getBatteryInfo(false /* charging */, false /* enhanced */, true /* estimate */); 329 assertHistoryAndLinearProjection(info); 330 331 info = getBatteryInfo(false /* charging */, true /* enhanced */, false /* estimate */); 332 assertOnlyHistory(info); 333 334 info = getBatteryInfo(false /* charging */, true /* enhanced */, true /* estimate */); 335 assertHistoryAndEnhancedProjection(info); 336 337 info = getBatteryInfo(true /* charging */, false /* enhanced */, false /* estimate */); 338 assertOnlyHistory(info); 339 340 info = getBatteryInfo(true /* charging */, false /* enhanced */, true /* estimate */); 341 assertHistoryAndLinearProjection(info); 342 343 info = getBatteryInfo(true /* charging */, true /* enhanced */, false /* estimate */); 344 assertOnlyHistory(info); 345 346 // Linear projection for charging even in enhanced mode. 347 info = getBatteryInfo(true /* charging */, true /* enhanced */, true /* estimate */); 348 assertHistoryAndLinearProjection(info); 349 } 350 } 351