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; 24 import android.os.BatteryStats.HistoryItem; 25 import android.os.Bundle; 26 import android.os.SystemClock; 27 import android.text.format.Formatter; 28 import android.util.SparseIntArray; 29 30 import androidx.annotation.WorkerThread; 31 32 import com.android.internal.os.BatteryStatsHelper; 33 import com.android.settings.Utils; 34 import com.android.settings.overlay.FeatureFactory; 35 import com.android.settings.widget.UsageView; 36 import com.android.settingslib.R; 37 import com.android.settingslib.fuelgauge.Estimate; 38 import com.android.settingslib.fuelgauge.EstimateKt; 39 import com.android.settingslib.utils.PowerUtil; 40 import com.android.settingslib.utils.StringUtil; 41 42 public class BatteryInfo { 43 44 public CharSequence chargeLabel; 45 public CharSequence remainingLabel; 46 public int batteryLevel; 47 public boolean discharging = true; 48 public boolean isOverheated; 49 public long remainingTimeUs = 0; 50 public long averageTimeToDischarge = EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN; 51 public String batteryPercentString; 52 public String statusLabel; 53 public String suggestionLabel; 54 private boolean mCharging; 55 private BatteryStats mStats; 56 private static final String LOG_TAG = "BatteryInfo"; 57 private long timePeriod; 58 59 public interface Callback { onBatteryInfoLoaded(BatteryInfo info)60 void onBatteryInfoLoaded(BatteryInfo info); 61 } 62 bindHistory(final UsageView view, BatteryDataParser... parsers)63 public void bindHistory(final UsageView view, BatteryDataParser... parsers) { 64 final Context context = view.getContext(); 65 BatteryDataParser parser = new BatteryDataParser() { 66 SparseIntArray points = new SparseIntArray(); 67 long startTime; 68 int lastTime = -1; 69 byte lastLevel; 70 71 @Override 72 public void onParsingStarted(long startTime, long endTime) { 73 this.startTime = startTime; 74 timePeriod = endTime - startTime; 75 view.clearPaths(); 76 // Initially configure the graph for history only. 77 view.configureGraph((int) timePeriod, 100); 78 } 79 80 @Override 81 public void onDataPoint(long time, HistoryItem record) { 82 lastTime = (int) time; 83 lastLevel = record.batteryLevel; 84 points.put(lastTime, lastLevel); 85 } 86 87 @Override 88 public void onDataGap() { 89 if (points.size() > 1) { 90 view.addPath(points); 91 } 92 points.clear(); 93 } 94 95 @Override 96 public void onParsingDone() { 97 onDataGap(); 98 99 // Add projection if we have an estimate. 100 if (remainingTimeUs != 0) { 101 PowerUsageFeatureProvider provider = FeatureFactory.getFactory(context) 102 .getPowerUsageFeatureProvider(context); 103 if (!mCharging && provider.isEnhancedBatteryPredictionEnabled(context)) { 104 points = provider.getEnhancedBatteryPredictionCurve(context, startTime); 105 } else { 106 // Linear extrapolation. 107 if (lastTime >= 0) { 108 points.put(lastTime, lastLevel); 109 points.put((int) (timePeriod + 110 PowerUtil.convertUsToMs(remainingTimeUs)), 111 mCharging ? 100 : 0); 112 } 113 } 114 } 115 116 // If we have a projection, reconfigure the graph to show it. 117 if (points != null && points.size() > 0) { 118 int maxTime = points.keyAt(points.size() - 1); 119 view.configureGraph(maxTime, 100); 120 view.addProjectedPath(points); 121 } 122 } 123 }; 124 BatteryDataParser[] parserList = new BatteryDataParser[parsers.length + 1]; 125 for (int i = 0; i < parsers.length; i++) { 126 parserList[i] = parsers[i]; 127 } 128 parserList[parsers.length] = parser; 129 parse(mStats, parserList); 130 String timeString = context.getString(R.string.charge_length_format, 131 Formatter.formatShortElapsedTime(context, timePeriod)); 132 String remaining = ""; 133 if (remainingTimeUs != 0) { 134 remaining = context.getString(R.string.remaining_length_format, 135 Formatter.formatShortElapsedTime(context, remainingTimeUs / 1000)); 136 } 137 view.setBottomLabels(new CharSequence[]{timeString, remaining}); 138 } 139 getBatteryInfo(final Context context, final Callback callback)140 public static void getBatteryInfo(final Context context, final Callback callback) { 141 BatteryInfo.getBatteryInfo(context, callback, null /* statsHelper */, 142 false /* shortString */); 143 } 144 getBatteryInfo(final Context context, final Callback callback, boolean shortString)145 public static void getBatteryInfo(final Context context, final Callback callback, 146 boolean shortString) { 147 BatteryInfo.getBatteryInfo(context, callback, null /* statsHelper */, shortString); 148 } 149 getBatteryInfo(final Context context, final Callback callback, final BatteryStatsHelper statsHelper, boolean shortString)150 public static void getBatteryInfo(final Context context, final Callback callback, 151 final BatteryStatsHelper statsHelper, boolean shortString) { 152 new AsyncTask<Void, Void, BatteryInfo>() { 153 @Override 154 protected BatteryInfo doInBackground(Void... params) { 155 return getBatteryInfo(context, statsHelper, shortString); 156 } 157 158 @Override 159 protected void onPostExecute(BatteryInfo batteryInfo) { 160 final long startTime = System.currentTimeMillis(); 161 callback.onBatteryInfoLoaded(batteryInfo); 162 BatteryUtils.logRuntime(LOG_TAG, "time for callback", startTime); 163 } 164 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 165 } 166 getBatteryInfo(final Context context, final BatteryStatsHelper statsHelper, boolean shortString)167 public static BatteryInfo getBatteryInfo(final Context context, 168 final BatteryStatsHelper statsHelper, boolean shortString) { 169 final BatteryStats stats; 170 final long batteryStatsTime = System.currentTimeMillis(); 171 if (statsHelper == null) { 172 final BatteryStatsHelper localStatsHelper = new BatteryStatsHelper(context, 173 true); 174 localStatsHelper.create((Bundle) null); 175 stats = localStatsHelper.getStats(); 176 } else { 177 stats = statsHelper.getStats(); 178 } 179 BatteryUtils.logRuntime(LOG_TAG, "time for getStats", batteryStatsTime); 180 181 final long startTime = System.currentTimeMillis(); 182 PowerUsageFeatureProvider provider = 183 FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context); 184 final long elapsedRealtimeUs = 185 PowerUtil.convertMsToUs(SystemClock.elapsedRealtime()); 186 187 final Intent batteryBroadcast = context.registerReceiver(null, 188 new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 189 // 0 means we are discharging, anything else means charging 190 final boolean discharging = 191 batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0; 192 193 if (discharging && provider != null 194 && provider.isEnhancedBatteryPredictionEnabled(context)) { 195 Estimate estimate = provider.getEnhancedBatteryPrediction(context); 196 if (estimate != null) { 197 Estimate.storeCachedEstimate(context, estimate); 198 BatteryUtils 199 .logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime); 200 return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, 201 estimate, elapsedRealtimeUs, shortString); 202 } 203 } 204 final long prediction = discharging 205 ? stats.computeBatteryTimeRemaining(elapsedRealtimeUs) : 0; 206 final Estimate estimate = new Estimate( 207 PowerUtil.convertUsToMs(prediction), 208 false, /* isBasedOnUsage */ 209 EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); 210 BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime); 211 return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, 212 estimate, elapsedRealtimeUs, shortString); 213 } 214 215 @WorkerThread getBatteryInfoOld(Context context, Intent batteryBroadcast, BatteryStats stats, long elapsedRealtimeUs, boolean shortString)216 public static BatteryInfo getBatteryInfoOld(Context context, Intent batteryBroadcast, 217 BatteryStats stats, long elapsedRealtimeUs, boolean shortString) { 218 Estimate estimate = new Estimate( 219 PowerUtil.convertUsToMs(stats.computeBatteryTimeRemaining(elapsedRealtimeUs)), 220 false, 221 EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); 222 return getBatteryInfo(context, batteryBroadcast, stats, estimate, elapsedRealtimeUs, 223 shortString); 224 } 225 226 @WorkerThread getBatteryInfo(Context context, Intent batteryBroadcast, BatteryStats stats, Estimate estimate, long elapsedRealtimeUs, boolean shortString)227 public static BatteryInfo getBatteryInfo(Context context, Intent batteryBroadcast, 228 BatteryStats stats, Estimate estimate, long elapsedRealtimeUs, boolean shortString) { 229 final long startTime = System.currentTimeMillis(); 230 BatteryInfo info = new BatteryInfo(); 231 info.mStats = stats; 232 info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast); 233 info.batteryPercentString = Utils.formatPercentage(info.batteryLevel); 234 info.mCharging = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; 235 info.averageTimeToDischarge = estimate.getAverageDischargeTime(); 236 info.isOverheated = batteryBroadcast.getIntExtra( 237 BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN) 238 == BatteryManager.BATTERY_HEALTH_OVERHEAT; 239 240 info.statusLabel = Utils.getBatteryStatus(context, batteryBroadcast); 241 if (!info.mCharging) { 242 updateBatteryInfoDischarging(context, shortString, estimate, info); 243 } else { 244 updateBatteryInfoCharging(context, batteryBroadcast, stats, elapsedRealtimeUs, info); 245 } 246 BatteryUtils.logRuntime(LOG_TAG, "time for getBatteryInfo", startTime); 247 return info; 248 } 249 updateBatteryInfoCharging(Context context, Intent batteryBroadcast, BatteryStats stats, long elapsedRealtimeUs, BatteryInfo info)250 private static void updateBatteryInfoCharging(Context context, Intent batteryBroadcast, 251 BatteryStats stats, long elapsedRealtimeUs, BatteryInfo info) { 252 final Resources resources = context.getResources(); 253 final long chargeTime = stats.computeChargeTimeRemaining(elapsedRealtimeUs); 254 final int status = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_STATUS, 255 BatteryManager.BATTERY_STATUS_UNKNOWN); 256 info.discharging = false; 257 info.suggestionLabel = null; 258 if (info.isOverheated && status != BatteryManager.BATTERY_STATUS_FULL) { 259 info.remainingLabel = null; 260 int chargingLimitedResId = R.string.power_charging_limited; 261 info.chargeLabel = 262 context.getString(chargingLimitedResId, info.batteryPercentString); 263 } else if (chargeTime > 0 && status != BatteryManager.BATTERY_STATUS_FULL) { 264 info.remainingTimeUs = chargeTime; 265 CharSequence timeString = StringUtil.formatElapsedTime(context, 266 PowerUtil.convertUsToMs(info.remainingTimeUs), false /* withSeconds */); 267 int resId = R.string.power_charging_duration; 268 info.remainingLabel = context.getString( 269 R.string.power_remaining_charging_duration_only, timeString); 270 info.chargeLabel = context.getString(resId, info.batteryPercentString, timeString); 271 } else { 272 final String chargeStatusLabel = Utils.getBatteryStatus(context, batteryBroadcast); 273 info.remainingLabel = null; 274 info.chargeLabel = info.batteryLevel == 100 ? info.batteryPercentString : 275 resources.getString(R.string.power_charging, info.batteryPercentString, 276 chargeStatusLabel.toLowerCase()); 277 } 278 } 279 updateBatteryInfoDischarging(Context context, boolean shortString, Estimate estimate, BatteryInfo info)280 private static void updateBatteryInfoDischarging(Context context, boolean shortString, 281 Estimate estimate, BatteryInfo info) { 282 final long drainTimeUs = PowerUtil.convertMsToUs(estimate.getEstimateMillis()); 283 if (drainTimeUs > 0) { 284 info.remainingTimeUs = drainTimeUs; 285 info.remainingLabel = PowerUtil.getBatteryRemainingStringFormatted( 286 context, 287 PowerUtil.convertUsToMs(drainTimeUs), 288 null /* percentageString */, 289 estimate.isBasedOnUsage() && !shortString 290 ); 291 info.chargeLabel = PowerUtil.getBatteryRemainingStringFormatted( 292 context, 293 PowerUtil.convertUsToMs(drainTimeUs), 294 info.batteryPercentString, 295 estimate.isBasedOnUsage() && !shortString 296 ); 297 info.suggestionLabel = PowerUtil.getBatteryTipStringFormatted( 298 context, PowerUtil.convertUsToMs(drainTimeUs)); 299 } else { 300 info.remainingLabel = null; 301 info.suggestionLabel = null; 302 info.chargeLabel = info.batteryPercentString; 303 } 304 } 305 306 public interface BatteryDataParser { onParsingStarted(long startTime, long endTime)307 void onParsingStarted(long startTime, long endTime); 308 onDataPoint(long time, HistoryItem record)309 void onDataPoint(long time, HistoryItem record); 310 onDataGap()311 void onDataGap(); 312 onParsingDone()313 void onParsingDone(); 314 } 315 parse(BatteryStats stats, BatteryDataParser... parsers)316 public static void parse(BatteryStats stats, BatteryDataParser... parsers) { 317 long startWalltime = 0; 318 long endWalltime = 0; 319 long historyStart = 0; 320 long historyEnd = 0; 321 long curWalltime = startWalltime; 322 long lastWallTime = 0; 323 long lastRealtime = 0; 324 int lastInteresting = 0; 325 int pos = 0; 326 boolean first = true; 327 if (stats.startIteratingHistoryLocked()) { 328 final HistoryItem rec = new HistoryItem(); 329 while (stats.getNextHistoryLocked(rec)) { 330 pos++; 331 if (first) { 332 first = false; 333 historyStart = rec.time; 334 } 335 if (rec.cmd == HistoryItem.CMD_CURRENT_TIME 336 || rec.cmd == HistoryItem.CMD_RESET) { 337 // If there is a ridiculously large jump in time, then we won't be 338 // able to create a good chart with that data, so just ignore the 339 // times we got before and pretend like our data extends back from 340 // the time we have now. 341 // Also, if we are getting a time change and we are less than 5 minutes 342 // since the start of the history real time, then also use this new 343 // time to compute the base time, since whatever time we had before is 344 // pretty much just noise. 345 if (rec.currentTime > (lastWallTime + (180 * 24 * 60 * 60 * 1000L)) 346 || rec.time < (historyStart + (5 * 60 * 1000L))) { 347 startWalltime = 0; 348 } 349 lastWallTime = rec.currentTime; 350 lastRealtime = rec.time; 351 if (startWalltime == 0) { 352 startWalltime = lastWallTime - (lastRealtime - historyStart); 353 } 354 } 355 if (rec.isDeltaData()) { 356 lastInteresting = pos; 357 historyEnd = rec.time; 358 } 359 } 360 } 361 stats.finishIteratingHistoryLocked(); 362 endWalltime = lastWallTime + historyEnd - lastRealtime; 363 364 int i = 0; 365 final int N = lastInteresting; 366 367 for (int j = 0; j < parsers.length; j++) { 368 parsers[j].onParsingStarted(startWalltime, endWalltime); 369 } 370 if (endWalltime > startWalltime && stats.startIteratingHistoryLocked()) { 371 final HistoryItem rec = new HistoryItem(); 372 while (stats.getNextHistoryLocked(rec) && i < N) { 373 if (rec.isDeltaData()) { 374 curWalltime += rec.time - lastRealtime; 375 lastRealtime = rec.time; 376 long x = (curWalltime - startWalltime); 377 if (x < 0) { 378 x = 0; 379 } 380 for (int j = 0; j < parsers.length; j++) { 381 parsers[j].onDataPoint(x, rec); 382 } 383 } else { 384 long lastWalltime = curWalltime; 385 if (rec.cmd == HistoryItem.CMD_CURRENT_TIME 386 || rec.cmd == HistoryItem.CMD_RESET) { 387 if (rec.currentTime >= startWalltime) { 388 curWalltime = rec.currentTime; 389 } else { 390 curWalltime = startWalltime + (rec.time - historyStart); 391 } 392 lastRealtime = rec.time; 393 } 394 395 if (rec.cmd != HistoryItem.CMD_OVERFLOW 396 && (rec.cmd != HistoryItem.CMD_CURRENT_TIME 397 || Math.abs(lastWalltime - curWalltime) > (60 * 60 * 1000))) { 398 for (int j = 0; j < parsers.length; j++) { 399 parsers[j].onDataGap(); 400 } 401 } 402 } 403 i++; 404 } 405 } 406 407 stats.finishIteratingHistoryLocked(); 408 409 for (int j = 0; j < parsers.length; j++) { 410 parsers[j].onParsingDone(); 411 } 412 } 413 } 414