1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.settings.fuelgauge; 16 17 import android.content.Context; 18 import android.content.Intent; 19 import android.content.IntentFilter; 20 import android.content.res.Resources; 21 import android.os.AsyncTask; 22 import android.os.BatteryManager; 23 import android.os.BatteryStats.HistoryItem; 24 import android.os.BatteryStatsManager; 25 import android.os.BatteryUsageStats; 26 import android.os.SystemClock; 27 import android.text.format.Formatter; 28 import android.util.Log; 29 import android.util.SparseIntArray; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.WorkerThread; 34 35 import com.android.internal.os.BatteryStatsHistoryIterator; 36 import com.android.settings.Utils; 37 import com.android.settings.overlay.FeatureFactory; 38 import com.android.settings.widget.UsageView; 39 import com.android.settingslib.R; 40 import com.android.settingslib.fuelgauge.Estimate; 41 import com.android.settingslib.fuelgauge.EstimateKt; 42 import com.android.settingslib.utils.PowerUtil; 43 import com.android.settingslib.utils.StringUtil; 44 45 public class BatteryInfo { 46 private static final String TAG = "BatteryInfo"; 47 48 public CharSequence chargeLabel; 49 public CharSequence remainingLabel; 50 public int batteryLevel; 51 public int batteryStatus; 52 public int pluggedStatus; 53 public boolean discharging = true; 54 public boolean isOverheated; 55 public long remainingTimeUs = 0; 56 public long averageTimeToDischarge = EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN; 57 public String batteryPercentString; 58 public String statusLabel; 59 public String suggestionLabel; 60 private boolean mCharging; 61 private BatteryUsageStats mBatteryUsageStats; 62 private static final String LOG_TAG = "BatteryInfo"; 63 private long timePeriod; 64 65 public interface Callback { onBatteryInfoLoaded(BatteryInfo info)66 void onBatteryInfoLoaded(BatteryInfo info); 67 } 68 bindHistory(final UsageView view, BatteryDataParser... parsers)69 public void bindHistory(final UsageView view, BatteryDataParser... parsers) { 70 final Context context = view.getContext(); 71 BatteryDataParser parser = new BatteryDataParser() { 72 SparseIntArray points = new SparseIntArray(); 73 long startTime; 74 int lastTime = -1; 75 byte lastLevel; 76 77 @Override 78 public void onParsingStarted(long startTime, long endTime) { 79 this.startTime = startTime; 80 timePeriod = endTime - startTime; 81 view.clearPaths(); 82 // Initially configure the graph for history only. 83 view.configureGraph((int) timePeriod, 100); 84 } 85 86 @Override 87 public void onDataPoint(long time, HistoryItem record) { 88 lastTime = (int) time; 89 lastLevel = record.batteryLevel; 90 points.put(lastTime, lastLevel); 91 } 92 93 @Override 94 public void onDataGap() { 95 if (points.size() > 1) { 96 view.addPath(points); 97 } 98 points.clear(); 99 } 100 101 @Override 102 public void onParsingDone() { 103 onDataGap(); 104 105 // Add projection if we have an estimate. 106 if (remainingTimeUs != 0) { 107 PowerUsageFeatureProvider provider = FeatureFactory.getFactory(context) 108 .getPowerUsageFeatureProvider(context); 109 if (!mCharging && provider.isEnhancedBatteryPredictionEnabled(context)) { 110 points = provider.getEnhancedBatteryPredictionCurve(context, startTime); 111 } else { 112 // Linear extrapolation. 113 if (lastTime >= 0) { 114 points.put(lastTime, lastLevel); 115 points.put((int) (timePeriod + 116 PowerUtil.convertUsToMs(remainingTimeUs)), 117 mCharging ? 100 : 0); 118 } 119 } 120 } 121 122 // If we have a projection, reconfigure the graph to show it. 123 if (points != null && points.size() > 0) { 124 int maxTime = points.keyAt(points.size() - 1); 125 view.configureGraph(maxTime, 100); 126 view.addProjectedPath(points); 127 } 128 } 129 }; 130 BatteryDataParser[] parserList = new BatteryDataParser[parsers.length + 1]; 131 for (int i = 0; i < parsers.length; i++) { 132 parserList[i] = parsers[i]; 133 } 134 parserList[parsers.length] = parser; 135 parseBatteryHistory(parserList); 136 String timeString = context.getString(R.string.charge_length_format, 137 Formatter.formatShortElapsedTime(context, timePeriod)); 138 String remaining = ""; 139 if (remainingTimeUs != 0) { 140 remaining = context.getString(R.string.remaining_length_format, 141 Formatter.formatShortElapsedTime(context, remainingTimeUs / 1000)); 142 } 143 view.setBottomLabels(new CharSequence[]{timeString, remaining}); 144 } 145 getBatteryInfo(final Context context, final Callback callback, boolean shortString)146 public static void getBatteryInfo(final Context context, final Callback callback, 147 boolean shortString) { 148 BatteryInfo.getBatteryInfo(context, callback, /* batteryUsageStats */ null, shortString); 149 } 150 getBatteryInfo(final Context context, final Callback callback, @Nullable final BatteryUsageStats batteryUsageStats, boolean shortString)151 public static void getBatteryInfo(final Context context, final Callback callback, 152 @Nullable final BatteryUsageStats batteryUsageStats, 153 boolean shortString) { 154 new AsyncTask<Void, Void, BatteryInfo>() { 155 @Override 156 protected BatteryInfo doInBackground(Void... params) { 157 boolean shouldCloseBatteryUsageStats = false; 158 BatteryUsageStats stats; 159 if (batteryUsageStats != null) { 160 stats = batteryUsageStats; 161 } else { 162 try { 163 stats = context.getSystemService(BatteryStatsManager.class) 164 .getBatteryUsageStats(); 165 shouldCloseBatteryUsageStats = true; 166 } catch (RuntimeException e) { 167 Log.e(TAG, "getBatteryInfo() from getBatteryUsageStats()", e); 168 // Use default BatteryUsageStats. 169 stats = new BatteryUsageStats.Builder(new String[0]).build(); 170 } 171 } 172 final BatteryInfo batteryInfo = 173 getBatteryInfo(context, stats, shortString); 174 if (shouldCloseBatteryUsageStats) { 175 try { 176 stats.close(); 177 } catch (Exception e) { 178 Log.e(TAG, "BatteryUsageStats.close() failed", e); 179 } 180 } 181 return batteryInfo; 182 } 183 184 @Override 185 protected void onPostExecute(BatteryInfo batteryInfo) { 186 final long startTime = System.currentTimeMillis(); 187 callback.onBatteryInfoLoaded(batteryInfo); 188 BatteryUtils.logRuntime(LOG_TAG, "time for callback", startTime); 189 } 190 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 191 } 192 193 /** 194 * Creates a BatteryInfo based on BatteryUsageStats 195 */ 196 @WorkerThread getBatteryInfo(final Context context, @NonNull final BatteryUsageStats batteryUsageStats, boolean shortString)197 public static BatteryInfo getBatteryInfo(final Context context, 198 @NonNull final BatteryUsageStats batteryUsageStats, boolean shortString) { 199 final long batteryStatsTime = System.currentTimeMillis(); 200 BatteryUtils.logRuntime(LOG_TAG, "time for getStats", batteryStatsTime); 201 202 final long startTime = System.currentTimeMillis(); 203 PowerUsageFeatureProvider provider = 204 FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context); 205 final long elapsedRealtimeUs = 206 PowerUtil.convertMsToUs(SystemClock.elapsedRealtime()); 207 208 final Intent batteryBroadcast = context.registerReceiver(null, 209 new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 210 // 0 means we are discharging, anything else means charging 211 final boolean discharging = 212 batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0; 213 214 if (discharging && provider != null 215 && provider.isEnhancedBatteryPredictionEnabled(context)) { 216 Estimate estimate = provider.getEnhancedBatteryPrediction(context); 217 if (estimate != null) { 218 Estimate.storeCachedEstimate(context, estimate); 219 BatteryUtils 220 .logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime); 221 return BatteryInfo.getBatteryInfo(context, batteryBroadcast, batteryUsageStats, 222 estimate, elapsedRealtimeUs, shortString); 223 } 224 } 225 final long prediction = discharging ? batteryUsageStats.getBatteryTimeRemainingMs() : 0; 226 final Estimate estimate = new Estimate( 227 prediction, 228 false, /* isBasedOnUsage */ 229 EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); 230 BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime); 231 return BatteryInfo.getBatteryInfo(context, batteryBroadcast, batteryUsageStats, 232 estimate, elapsedRealtimeUs, shortString); 233 } 234 235 @WorkerThread getBatteryInfoOld(Context context, Intent batteryBroadcast, BatteryUsageStats batteryUsageStats, long elapsedRealtimeUs, boolean shortString)236 public static BatteryInfo getBatteryInfoOld(Context context, Intent batteryBroadcast, 237 BatteryUsageStats batteryUsageStats, long elapsedRealtimeUs, boolean shortString) { 238 Estimate estimate = new Estimate( 239 batteryUsageStats.getBatteryTimeRemainingMs(), 240 false, 241 EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); 242 return getBatteryInfo(context, batteryBroadcast, batteryUsageStats, estimate, 243 elapsedRealtimeUs, shortString); 244 } 245 246 @WorkerThread getBatteryInfo(Context context, Intent batteryBroadcast, @NonNull BatteryUsageStats batteryUsageStats, Estimate estimate, long elapsedRealtimeUs, boolean shortString)247 public static BatteryInfo getBatteryInfo(Context context, Intent batteryBroadcast, 248 @NonNull BatteryUsageStats batteryUsageStats, Estimate estimate, 249 long elapsedRealtimeUs, boolean shortString) { 250 final long startTime = System.currentTimeMillis(); 251 final boolean isCompactStatus = context.getResources().getBoolean( 252 com.android.settings.R.bool.config_use_compact_battery_status); 253 BatteryInfo info = new BatteryInfo(); 254 info.mBatteryUsageStats = batteryUsageStats; 255 info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast); 256 info.batteryPercentString = Utils.formatPercentage(info.batteryLevel); 257 info.pluggedStatus = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); 258 info.mCharging = info.pluggedStatus != 0; 259 info.averageTimeToDischarge = estimate.getAverageDischargeTime(); 260 info.isOverheated = batteryBroadcast.getIntExtra( 261 BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN) 262 == BatteryManager.BATTERY_HEALTH_OVERHEAT; 263 264 info.statusLabel = Utils.getBatteryStatus(context, batteryBroadcast, isCompactStatus); 265 info.batteryStatus = batteryBroadcast.getIntExtra( 266 BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN); 267 if (!info.mCharging) { 268 updateBatteryInfoDischarging(context, shortString, estimate, info); 269 } else { 270 updateBatteryInfoCharging(context, batteryBroadcast, batteryUsageStats, 271 info, isCompactStatus); 272 } 273 BatteryUtils.logRuntime(LOG_TAG, "time for getBatteryInfo", startTime); 274 return info; 275 } 276 updateBatteryInfoCharging(Context context, Intent batteryBroadcast, BatteryUsageStats stats, BatteryInfo info, boolean compactStatus)277 private static void updateBatteryInfoCharging(Context context, Intent batteryBroadcast, 278 BatteryUsageStats stats, BatteryInfo info, boolean compactStatus) { 279 final Resources resources = context.getResources(); 280 final long chargeTimeMs = stats.getChargeTimeRemainingMs(); 281 final int status = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_STATUS, 282 BatteryManager.BATTERY_STATUS_UNKNOWN); 283 info.discharging = false; 284 info.suggestionLabel = null; 285 int dockDefenderMode = BatteryUtils.getCurrentDockDefenderMode(context, info); 286 if ((info.isOverheated && status != BatteryManager.BATTERY_STATUS_FULL 287 && dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED) 288 || dockDefenderMode == BatteryUtils.DockDefenderMode.ACTIVE) { 289 // Battery defender active, battery charging paused 290 info.remainingLabel = null; 291 int chargingLimitedResId = R.string.power_charging_limited; 292 info.chargeLabel = context.getString(chargingLimitedResId, info.batteryPercentString); 293 } else if ((chargeTimeMs > 0 && status != BatteryManager.BATTERY_STATUS_FULL 294 && dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED) 295 || dockDefenderMode == BatteryUtils.DockDefenderMode.TEMPORARILY_BYPASSED) { 296 // Battery is charging to full 297 info.remainingTimeUs = PowerUtil.convertMsToUs(chargeTimeMs); 298 final CharSequence timeString = StringUtil.formatElapsedTime(context, 299 (double) PowerUtil.convertUsToMs(info.remainingTimeUs), false /* withSeconds */, 300 true /* collapseTimeUnit */); 301 int resId = R.string.power_charging_duration; 302 info.remainingLabel = context.getString(R.string.power_remaining_charging_duration_only, 303 timeString); 304 info.chargeLabel = context.getString(resId, info.batteryPercentString, timeString); 305 } else if (dockDefenderMode == BatteryUtils.DockDefenderMode.FUTURE_BYPASS) { 306 // Dock defender will be triggered in the future, charging will be optimized. 307 info.chargeLabel = context.getString(R.string.power_charging_future_paused, 308 info.batteryPercentString); 309 } else { 310 final String chargeStatusLabel = Utils.getBatteryStatus(context, batteryBroadcast, 311 compactStatus); 312 info.remainingLabel = null; 313 info.chargeLabel = info.batteryLevel == 100 ? info.batteryPercentString : 314 resources.getString(R.string.power_charging, info.batteryPercentString, 315 chargeStatusLabel.toLowerCase()); 316 } 317 } 318 updateBatteryInfoDischarging(Context context, boolean shortString, Estimate estimate, BatteryInfo info)319 private static void updateBatteryInfoDischarging(Context context, boolean shortString, 320 Estimate estimate, BatteryInfo info) { 321 final long drainTimeUs = PowerUtil.convertMsToUs(estimate.getEstimateMillis()); 322 if (drainTimeUs > 0) { 323 info.remainingTimeUs = drainTimeUs; 324 info.remainingLabel = PowerUtil.getBatteryRemainingStringFormatted( 325 context, 326 PowerUtil.convertUsToMs(drainTimeUs), 327 null /* percentageString */, 328 false /* basedOnUsage */ 329 ); 330 info.chargeLabel = PowerUtil.getBatteryRemainingStringFormatted( 331 context, 332 PowerUtil.convertUsToMs(drainTimeUs), 333 info.batteryPercentString, 334 estimate.isBasedOnUsage() && !shortString 335 ); 336 info.suggestionLabel = PowerUtil.getBatteryTipStringFormatted( 337 context, PowerUtil.convertUsToMs(drainTimeUs)); 338 } else { 339 info.remainingLabel = null; 340 info.suggestionLabel = null; 341 info.chargeLabel = info.batteryPercentString; 342 } 343 } 344 345 public interface BatteryDataParser { onParsingStarted(long startTime, long endTime)346 void onParsingStarted(long startTime, long endTime); 347 onDataPoint(long time, HistoryItem record)348 void onDataPoint(long time, HistoryItem record); 349 onDataGap()350 void onDataGap(); 351 onParsingDone()352 void onParsingDone(); 353 } 354 355 /** 356 * Iterates over battery history included in the BatteryUsageStats that this object 357 * was initialized with. 358 */ parseBatteryHistory(BatteryDataParser... parsers)359 public void parseBatteryHistory(BatteryDataParser... parsers) { 360 long startWalltime = 0; 361 long endWalltime = 0; 362 long historyStart = 0; 363 long historyEnd = 0; 364 long curWalltime = startWalltime; 365 long lastWallTime = 0; 366 long lastRealtime = 0; 367 int lastInteresting = 0; 368 int pos = 0; 369 boolean first = true; 370 final BatteryStatsHistoryIterator iterator1 = 371 mBatteryUsageStats.iterateBatteryStatsHistory(); 372 final HistoryItem rec = new HistoryItem(); 373 while (iterator1.next(rec)) { 374 pos++; 375 if (first) { 376 first = false; 377 historyStart = rec.time; 378 } 379 if (rec.cmd == HistoryItem.CMD_CURRENT_TIME 380 || rec.cmd == HistoryItem.CMD_RESET) { 381 // If there is a ridiculously large jump in time, then we won't be 382 // able to create a good chart with that data, so just ignore the 383 // times we got before and pretend like our data extends back from 384 // the time we have now. 385 // Also, if we are getting a time change and we are less than 5 minutes 386 // since the start of the history real time, then also use this new 387 // time to compute the base time, since whatever time we had before is 388 // pretty much just noise. 389 if (rec.currentTime > (lastWallTime + (180 * 24 * 60 * 60 * 1000L)) 390 || rec.time < (historyStart + (5 * 60 * 1000L))) { 391 startWalltime = 0; 392 } 393 lastWallTime = rec.currentTime; 394 lastRealtime = rec.time; 395 if (startWalltime == 0) { 396 startWalltime = lastWallTime - (lastRealtime - historyStart); 397 } 398 } 399 if (rec.isDeltaData()) { 400 lastInteresting = pos; 401 historyEnd = rec.time; 402 } 403 } 404 405 endWalltime = lastWallTime + historyEnd - lastRealtime; 406 407 int i = 0; 408 final int N = lastInteresting; 409 410 for (int j = 0; j < parsers.length; j++) { 411 parsers[j].onParsingStarted(startWalltime, endWalltime); 412 } 413 414 if (endWalltime > startWalltime) { 415 final BatteryStatsHistoryIterator iterator2 = 416 mBatteryUsageStats.iterateBatteryStatsHistory(); 417 while (iterator2.next(rec) && i < N) { 418 if (rec.isDeltaData()) { 419 curWalltime += rec.time - lastRealtime; 420 lastRealtime = rec.time; 421 long x = (curWalltime - startWalltime); 422 if (x < 0) { 423 x = 0; 424 } 425 for (int j = 0; j < parsers.length; j++) { 426 parsers[j].onDataPoint(x, rec); 427 } 428 } else { 429 long lastWalltime = curWalltime; 430 if (rec.cmd == HistoryItem.CMD_CURRENT_TIME 431 || rec.cmd == HistoryItem.CMD_RESET) { 432 if (rec.currentTime >= startWalltime) { 433 curWalltime = rec.currentTime; 434 } else { 435 curWalltime = startWalltime + (rec.time - historyStart); 436 } 437 lastRealtime = rec.time; 438 } 439 440 if (rec.cmd != HistoryItem.CMD_OVERFLOW 441 && (rec.cmd != HistoryItem.CMD_CURRENT_TIME 442 || Math.abs(lastWalltime - curWalltime) > (60 * 60 * 1000))) { 443 for (int j = 0; j < parsers.length; j++) { 444 parsers[j].onDataGap(); 445 } 446 } 447 } 448 i++; 449 } 450 } 451 452 for (int j = 0; j < parsers.length; j++) { 453 parsers[j].onParsingDone(); 454 } 455 } 456 } 457