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