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